Skip to content

Commit c2f8dcf

Browse files
committed
fix(stitch-desktop): rebuild menu icons when OS appearance changes
- Add `light_glyphs` field to `MenuIcons` to track appearance state at bitmap generation time - Implement `refresh_for_appearance()` method to detect OS light/dark preference changes and rebuild icons with appropriate ink color - Call `refresh_for_appearance()` during status refresh polling and reapply all menu icons when appearance flips, since muda cannot mark custom RGBA icons as AppKit templates - Extract `MenuIcons::with_appearance()` private constructor to enable rebuilding with different ink colors - Add `reapply_all()` helper to update all tray menu icons after appearance changes - Rename `menu_ink()` to `ink_for()` with explicit `light_glyphs` parameter for clarity - Refactor `draw_keep_awake()` Z layout into `KEEP_AWAKE_ZS` constant array for testability - Add test `keep_awake_zs_fit_inside_canvas()` to ensure stroke and anti-aliasing fringe stay within 32×32 canvas bounds - Shift largest Z coordinates so stroke padding and anti-aliasing stay inside canvas
1 parent 8ed07df commit c2f8dcf

6 files changed

Lines changed: 121 additions & 12 deletions

File tree

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
27560e0d1d33e3e7c5511a2f00af61129d86f57c
1+
635225323dcc88edff878a0914247209c710ac0d

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.153
1+
0.1.154

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stitch-bot"
3-
version = "0.1.153"
3+
version = "0.1.154"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; market-makes the filler order book with signed UniswapX limit orders."
66
license = "AGPL-3.0-or-later"

src/bin/stitch-desktop/main.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ fn run() -> Result<()> {
187187
std::thread::sleep(std::time::Duration::from_secs(UPDATE_POLL_SECS));
188188
});
189189

190-
let menu_icons = MenuIcons::new();
190+
let mut menu_icons = MenuIcons::new();
191191
let panel_running = supervisor
192192
.lock()
193193
.map(|mut s| s.is_running())
@@ -487,6 +487,35 @@ fn run() -> Result<()> {
487487
}
488488
}
489489
Event::UserEvent(UserEvent::RefreshStatus) => {
490+
// Custom menu icons bake OS ink into RGBA. muda can't mark them
491+
// as AppKit templates, so rebuild when light/dark flips.
492+
if menu_icons.refresh_for_appearance() {
493+
let running = supervisor
494+
.lock()
495+
.map(|mut s| s.is_running())
496+
.unwrap_or(false);
497+
menu_icons::reapply_all(
498+
&menu_icons,
499+
&status_item,
500+
if running {
501+
StatusKind::Running
502+
} else {
503+
StatusKind::Stopped
504+
},
505+
&open_item,
506+
&pause_item,
507+
if running {
508+
ActionKind::Pause
509+
} else {
510+
ActionKind::Resume
511+
},
512+
&keep_awake_item,
513+
prefs.keep_awake,
514+
&update_item,
515+
&settings_item,
516+
&quit_item,
517+
);
518+
}
490519
sync_tray_menu(
491520
&status_item,
492521
&pause_item,

src/bin/stitch-desktop/menu_icons.rs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub enum ActionKind {
3030
const STROKE: f32 = 2.0;
3131

3232
pub struct MenuIcons {
33+
/// Matches [`prefer_light_glyphs`] at the time these bitmaps were drawn.
34+
light_glyphs: bool,
3335
dot_running: Icon,
3436
dot_stopped: Icon,
3537
open: Icon,
@@ -43,8 +45,13 @@ pub struct MenuIcons {
4345

4446
impl MenuIcons {
4547
pub fn new() -> Self {
46-
let ink = menu_ink();
48+
Self::with_appearance(prefer_light_glyphs())
49+
}
50+
51+
fn with_appearance(light_glyphs: bool) -> Self {
52+
let ink = ink_for(light_glyphs);
4753
Self {
54+
light_glyphs,
4855
// Outline rings; running adds a solid inner disc (radio-on).
4956
dot_running: draw_status_running(ink).expect("dot_running"),
5057
dot_stopped: draw_status_stopped(ink).expect("dot_stopped"),
@@ -57,6 +64,20 @@ impl MenuIcons {
5764
keep_awake: draw_keep_awake(ink).expect("keep_awake"),
5865
}
5966
}
67+
68+
/// Rebuild bitmaps when the OS light/dark preference changes.
69+
///
70+
/// muda doesn't expose a template-image flag for custom RGBA menu icons, so
71+
/// ink is baked into pixels. Call this from the periodic status poll (or an
72+
/// appearance notification) and reapply icons when it returns `true`.
73+
pub fn refresh_for_appearance(&mut self) -> bool {
74+
let light = prefer_light_glyphs();
75+
if light == self.light_glyphs {
76+
return false;
77+
}
78+
*self = Self::with_appearance(light);
79+
true
80+
}
6081
}
6182

6283
pub fn status_item(text: &str, kind: StatusKind, icons: &MenuIcons) -> IconMenuItem {
@@ -120,14 +141,37 @@ fn action_bitmap(icons: &MenuIcons, kind: ActionKind) -> Icon {
120141
}
121142

122143
/// Ink for monochrome glyphs. Light glyphs on dark menus; dark glyphs on light.
123-
fn menu_ink() -> (u8, u8, u8) {
124-
if prefer_light_glyphs() {
144+
fn ink_for(light_glyphs: bool) -> (u8, u8, u8) {
145+
if light_glyphs {
125146
(0xf2, 0xf2, 0xf7)
126147
} else {
127148
(0x1c, 0x1c, 0x1e)
128149
}
129150
}
130151

152+
/// Re-stamp every tray-menu icon after [`MenuIcons::refresh_for_appearance`].
153+
pub fn reapply_all(
154+
icons: &MenuIcons,
155+
status: &IconMenuItem,
156+
status_kind: StatusKind,
157+
open: &IconMenuItem,
158+
pause: &IconMenuItem,
159+
pause_kind: ActionKind,
160+
keep_awake: &IconMenuItem,
161+
keep_awake_enabled: bool,
162+
update: &IconMenuItem,
163+
settings: &IconMenuItem,
164+
quit: &IconMenuItem,
165+
) {
166+
apply_status(status, status_kind, icons);
167+
apply_action(open, ActionKind::Open, icons);
168+
apply_action(pause, pause_kind, icons);
169+
apply_keep_awake(keep_awake, keep_awake_enabled, icons);
170+
apply_action(update, ActionKind::Update, icons);
171+
apply_action(settings, ActionKind::Show, icons);
172+
apply_action(quit, ActionKind::Quit, icons);
173+
}
174+
131175
fn prefer_light_glyphs() -> bool {
132176
#[cfg(target_os = "macos")]
133177
{
@@ -209,6 +253,33 @@ mod theme_ink_tests {
209253
}
210254
}
211255

256+
#[cfg(test)]
257+
mod keep_awake_layout_tests {
258+
use super::{KEEP_AWAKE_ZS, SIZE};
259+
260+
/// `stroke_line_aa` stamps circles of radius `thickness/2`; `coverage`
261+
/// softens another ~1 px beyond that. Both must stay inside the canvas.
262+
const AA_FRINGE: f32 = 1.0;
263+
264+
#[test]
265+
fn keep_awake_zs_fit_inside_canvas() {
266+
for &(x, y, size, thickness) in &KEEP_AWAKE_ZS {
267+
let half = thickness / 2.0;
268+
let x1 = x + size;
269+
let y1 = y + size * 0.85;
270+
let left = x - half - AA_FRINGE;
271+
let right = x1 + half + AA_FRINGE;
272+
let top = y - half - AA_FRINGE;
273+
let bottom = y1 + half + AA_FRINGE;
274+
assert!(
275+
left >= 0.0 && right < SIZE as f32 && top >= 0.0 && bottom < SIZE as f32,
276+
"Z at ({x},{y}) size={size} thickness={thickness} extends to \
277+
L={left} R={right} T={top} B={bottom} outside 0..{SIZE}"
278+
);
279+
}
280+
}
281+
}
282+
212283
// --- Shared 32×32 anti-aliased outline glyphs ---
213284

214285
const SIZE: u32 = 32;
@@ -284,13 +355,22 @@ fn draw_show(c: (u8, u8, u8)) -> Result<Icon, tray_icon::menu::BadIcon> {
284355
px.into_icon()
285356
}
286357

358+
/// Layout for the three rising Z's. Kept as data so tests can assert the
359+
/// round stroke + AA fringe stays inside the 32×32 canvas.
360+
const KEEP_AWAKE_ZS: [(f32, f32, f32, f32); 3] = [
361+
// (x, y, size, thickness)
362+
(5.0, 18.0, 7.0, 1.6),
363+
(11.0, 11.5, 9.0, 1.8),
364+
// Right edge: x+size + thickness/2 + AA fringe (1px) must stay < SIZE.
365+
(17.5, 5.0, 11.0, STROKE),
366+
];
367+
287368
/// Outlined zZZ sleep glyph for Keep awake (💤).
288369
fn draw_keep_awake(c: (u8, u8, u8)) -> Result<Icon, tray_icon::menu::BadIcon> {
289370
let mut px = Canvas::new();
290-
// Three Z's of increasing size, rising left → right like 💤.
291-
stroke_z(&mut px, 5.0, 18.0, 7.0, c, 1.6);
292-
stroke_z(&mut px, 11.5, 11.5, 9.5, c, 1.8);
293-
stroke_z(&mut px, 19.0, 4.5, 12.0, c, STROKE);
371+
for &(x, y, size, thickness) in &KEEP_AWAKE_ZS {
372+
stroke_z(&mut px, x, y, size, c, thickness);
373+
}
294374
px.into_icon()
295375
}
296376

0 commit comments

Comments
 (0)