Brand colours on the marks; round the app's corners; move the lights in

Simple Icons publishes each brand's own hex next to its glyph, and the
build was throwing it away. Tiles now carry it - Gmail red, Drive blue,
Chat green, Gemini violet - and the glyph is drawn black or white by the
tile's perceived brightness, since brand colours are picked to look right
rather than to carry a white mark. GitHub and Notion are near-black,
Snapchat is pure yellow, and a fixed white glyph loses one end of that.

An app's right corners are now rounded on its own layer. The container's
rounded-xl could never have clipped them: an app is a native view sitting
on top, not something the shell lays out, so it kept square corners and
overhung the curve. Only the right pair - the left edge butts against the
nav, and rounding it would notch the middle of the window.

The traffic lights move to (26, 24) and the drag strip deepens to match,
so they sit inside the window's margin instead of against its corner.

Adds Gemini, Claude, ChatGPT, Linear, ClickUp, HubSpot, Shopify and
Cloudflare to the host table.
This commit is contained in:
2026-09-01 14:51:20 +02:00
parent ada18d54ae
commit ffedb72b83
10 changed files with 175 additions and 43 deletions
+7 -1
View File
@@ -15,6 +15,8 @@ pub type Stage = (f64, f64, f64, f64);
pub struct AppState {
pub dir: PathBuf,
/// Corner radius the window's inner frame is currently drawn with.
pub radius: Mutex<f64>,
pub config: Mutex<Config>,
pub active: Mutex<Option<String>>,
pub stage: Mutex<Stage>,
@@ -65,6 +67,7 @@ pub fn build_state(handle: &AppHandle) -> Result<AppState, String> {
let config = config::load(&dir)?;
Ok(AppState {
dir,
radius: Mutex::new(12.0),
config: Mutex::new(config),
active: Mutex::new(None),
stage: Mutex::new((240.0, 38.0, 800.0, 600.0)),
@@ -88,13 +91,15 @@ pub fn set_stage(
y: f64,
width: f64,
height: f64,
radius: f64,
app: AppHandle,
state: State<'_, AppState>,
) {
let stage = (x, y, width.max(0.0), height.max(0.0));
*state.stage.lock().unwrap() = stage;
*state.radius.lock().unwrap() = radius;
let active = state.active.lock().unwrap().clone();
webviews::set_stage(&app, active.as_deref(), stage);
webviews::set_stage(&app, active.as_deref(), stage, radius);
}
/// Creates every app's webview: the active one first, the rest staggered.
@@ -122,6 +127,7 @@ pub fn bootstrap(app: AppHandle, state: State<'_, AppState>) -> Result<(), Strin
if let Some(id) = &first {
if let Some(a) = cfg.app(id) {
webviews::create(&app, a, &cfg, stage)?;
webviews::set_corner_radius(&app, &a.id, *state.radius.lock().unwrap());
*state.active.lock().unwrap() = Some(id.clone());
webviews::show_only(&app, Some(id), &cfg, stage);
}
+47 -1
View File
@@ -419,6 +419,41 @@ pub fn snapshot(_: &AppHandle, _: &str) -> Option<String> {
None
}
/// Rounds an app's right-hand corners to match the window's inner frame.
///
/// A `rounded-xl` on the container around it does nothing: the app is a native
/// view sitting on top, not something the shell lays out, so it keeps its own
/// square corners and overhangs the curve. The rounding has to go on its layer.
///
/// Only the right pair — the left edge butts against the nav, and rounding it
/// would cut a notch out of the middle of the window.
#[cfg(target_os = "macos")]
pub fn set_corner_radius(handle: &AppHandle, app_id: &str, radius: f64) {
use objc2::runtime::AnyObject;
let Some(wv) = handle.get_webview(&label_for(app_id)) else { return };
let _ = wv.with_webview(move |platform| unsafe {
let view = platform.inner() as *mut AnyObject;
if view.is_null() {
return;
}
let _: () = objc2::msg_send![view, setWantsLayer: true];
let layer: *mut AnyObject = objc2::msg_send![view, layer];
if layer.is_null() {
return;
}
// kCALayerMaxXMinYCorner | kCALayerMaxXMaxYCorner — both right corners,
// whichever way round the layer's Y axis happens to run.
let right_corners: usize = (1 << 1) | (1 << 3);
let _: () = objc2::msg_send![layer, setCornerRadius: radius];
let _: () = objc2::msg_send![layer, setMaskedCorners: right_corners];
let _: () = objc2::msg_send![layer, setMasksToBounds: radius > 0.0];
});
}
#[cfg(not(target_os = "macos"))]
pub fn set_corner_radius(_: &AppHandle, _: &str, _: f64) {}
/// Turns on WKWebView's two-finger back and forward swipes.
///
/// wry supports it but Tauri does not expose it, so it is set on the native
@@ -534,6 +569,10 @@ pub fn create(
Ok(())
}
fn radius(handle: &AppHandle) -> f64 {
*handle.state::<crate::commands::AppState>().radius.lock().unwrap()
}
/// Tells a page whether it is the one being looked at.
///
/// Separate from the view's real visibility on purpose — see the note in
@@ -558,6 +597,7 @@ pub fn show_only(
let _ = wv.set_size(LogicalSize::new(stage.2, stage.3));
let _ = wv.set_zoom(app.zoom);
let _ = wv.show();
set_corner_radius(handle, &app.id, radius(handle));
set_page_visibility(handle, &app.id, Some(app.id.as_str()) == app_id);
}
if let Some(id) = app_id {
@@ -570,12 +610,18 @@ pub fn show_only(
/// All of them, not just the visible one: they are all really on screen now,
/// stacked, so one left at a stale size would show around the edges of the
/// active app the moment the window grew.
pub fn set_stage(handle: &AppHandle, active: Option<&str>, stage: (f64, f64, f64, f64)) {
pub fn set_stage(
handle: &AppHandle,
active: Option<&str>,
stage: (f64, f64, f64, f64),
radius: f64,
) {
let cfg = handle.state::<crate::commands::AppState>().cfg();
for app in &cfg.apps {
let Some(wv) = handle.get_webview(&label_for(&app.id)) else { continue };
let _ = wv.set_position(LogicalPosition::new(stage.0, stage.1));
let _ = wv.set_size(LogicalSize::new(stage.2, stage.3));
set_corner_radius(handle, &app.id, radius);
}
if let Some(id) = active {
raise_to_front(handle, id);
+1 -1
View File
@@ -21,7 +21,7 @@
"center": true,
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"trafficLightPosition": { "x": 19, "y": 18 }
"trafficLightPosition": { "x": 26, "y": 24 }
}
],
"security": { "csp": null }