mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
fix(tray): keep the macOS icon a template image in every state
The attention state left template mode to carry the amber badge, which forced the glyph to a hardcoded mid-grey that was illegible on many menu bar appearances. Drop the badge (and the grey recolor) on macOS: the icon now stays a template glyph the system recolors for the bar, and agent status lives in the tooltip and tray menu. Windows/Linux keep the amber corner badge on their colored icon.
This commit is contained in:
+36
-13
@@ -1,12 +1,12 @@
|
||||
//! Tray bitmap rendering: the bundled SVGs rasterized with `resvg` (gpui's
|
||||
//! own SVG path only yields a tinted alpha mask, so the tray draws its own).
|
||||
//!
|
||||
//! Two states per platform:
|
||||
//! Per platform:
|
||||
//! - macOS: the outline terminal glyph (`tray.svg`) as a *template* image —
|
||||
//! the system recolors its alpha for light/dark menu bars. Attention swaps
|
||||
//! to a non-template variant: the glyph recolored to a mid-grey that reads
|
||||
//! on both bar appearances, plus an amber badge (template images can't
|
||||
//! carry color, so attention opts out of templating).
|
||||
//! the system recolors its alpha for light/dark menu bars, permanently.
|
||||
//! Attention never touches the bitmap (template images can't carry color,
|
||||
//! and leaving template mode made the glyph illegible); the tooltip and
|
||||
//! menu carry agent status instead (see `native.rs`).
|
||||
//! - Windows / Linux: the colored app icon (`app-icon.svg`); attention
|
||||
//! punches a transparent ring into the corner and fills an amber badge, so
|
||||
//! the badge separates from the orange tile behind it.
|
||||
@@ -37,22 +37,32 @@ const SIZE: u32 = 32;
|
||||
|
||||
/// The `Waiting` amber, same hue as the in-window status dot
|
||||
/// (`AgentStatus::dot_rgb`).
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const AMBER: (u8, u8, u8) = (0xF5, 0x9E, 0x0B);
|
||||
|
||||
/// Render the tray icon. `attention` = some agent is blocked on the user.
|
||||
/// `None` only on a malformed bundled SVG, i.e. never in practice — callers
|
||||
/// treat it as "no icon change".
|
||||
/// Render the tray icon: the template outline glyph, always — attention
|
||||
/// never touches the bitmap, so the icon stays a template image the system
|
||||
/// keeps legible on any bar. `None` only on a malformed bundled SVG, i.e.
|
||||
/// never in practice — callers treat it as "no icon change".
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn render() -> Option<RgbaImage> {
|
||||
let tree = usvg::Tree::from_data(GLYPH_SVG, &usvg::Options::default()).ok()?;
|
||||
let mut pixmap = tiny_skia::Pixmap::new(SIZE, SIZE)?;
|
||||
resvg::render(&tree, fit_center(&tree, SIZE), &mut pixmap.as_mut());
|
||||
Some(to_rgba(&pixmap))
|
||||
}
|
||||
|
||||
/// Render the tray icon. `attention` = some agent is blocked on the user —
|
||||
/// stamps the amber badge into the colored app icon. `None` only on a
|
||||
/// malformed bundled SVG, i.e. never in practice — callers treat it as "no
|
||||
/// icon change".
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(super) fn render(attention: bool) -> Option<RgbaImage> {
|
||||
let tree = usvg::Tree::from_data(GLYPH_SVG, &usvg::Options::default()).ok()?;
|
||||
let mut pixmap = tiny_skia::Pixmap::new(SIZE, SIZE)?;
|
||||
resvg::render(&tree, fit_center(&tree, SIZE), &mut pixmap.as_mut());
|
||||
|
||||
if attention {
|
||||
// macOS attention leaves template mode (color needs real RGB), so the
|
||||
// glyph must carry its own color: a mid-grey legible on both light
|
||||
// and dark menu bars. Colored platforms keep the icon's own colors.
|
||||
#[cfg(target_os = "macos")]
|
||||
recolor(&mut pixmap, (0x8E, 0x8E, 0x93));
|
||||
badge(&mut pixmap);
|
||||
}
|
||||
|
||||
@@ -202,6 +212,7 @@ fn recolor(pixmap: &mut tiny_skia::Pixmap, rgb: (u8, u8, u8)) {
|
||||
/// Stamp the amber attention badge in the top-right corner: first clear a
|
||||
/// slightly larger disc so the badge is ringed by transparency (separating it
|
||||
/// from whatever the glyph or a colored tile puts behind it), then fill.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn badge(pixmap: &mut tiny_skia::Pixmap) {
|
||||
let s = SIZE as f32;
|
||||
let (cx, cy) = (s * 0.78, s * 0.22);
|
||||
@@ -301,8 +312,20 @@ mod tests {
|
||||
assert_eq!((px[1], px[2]), (0, 0));
|
||||
}
|
||||
|
||||
/// The template glyph renders at the declared size with visible coverage.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn render_produces_template_glyph() {
|
||||
let img = render().unwrap();
|
||||
assert_eq!((img.width, img.height), (SIZE, SIZE));
|
||||
assert_eq!(img.data.len(), (SIZE * SIZE * 4) as usize);
|
||||
let covered = img.data.chunks_exact(4).filter(|p| p[3] > 0).count();
|
||||
assert!(covered > 0, "icon rendered fully transparent");
|
||||
}
|
||||
|
||||
/// Both tray states render at the declared size with visible coverage,
|
||||
/// and the attention badge actually changes the bitmap.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn render_produces_both_states() {
|
||||
let normal = render(false).unwrap();
|
||||
|
||||
+12
-6
@@ -1,10 +1,12 @@
|
||||
//! System tray / menu bar status item.
|
||||
//!
|
||||
//! The tray is the app's face outside the window: the icon flips to an
|
||||
//! attention state the moment any pane's coding agent blocks on the user
|
||||
//! (amber `Waiting`), and the menu lists every agent pane — click one to
|
||||
//! reveal it — plus window/notification/quit controls. Menu labels are
|
||||
//! English, matching the native app menus (`ui::theme::set_menus`).
|
||||
//! The tray is the app's face outside the window: on Windows/Linux the icon
|
||||
//! flips to an amber-badged attention state the moment any pane's coding
|
||||
//! agent blocks on the user (`Waiting`); on macOS the icon is a template
|
||||
//! image that stays calm in every state (legible on any bar) — agent status
|
||||
//! lives in the tooltip and menu instead. The menu lists every agent pane —
|
||||
//! click one to reveal it — plus window/notification/quit controls. Menu
|
||||
//! labels are English, matching the native app menus (`ui::theme::set_menus`).
|
||||
//!
|
||||
//! Platform split (see Cargo.toml for the why):
|
||||
//! - macOS / Windows: tauri's `tray-icon` (NSStatusItem / Shell_NotifyIcon),
|
||||
@@ -90,7 +92,10 @@ pub(crate) struct TraySnapshot {
|
||||
}
|
||||
|
||||
impl TraySnapshot {
|
||||
/// Whether any agent is blocked on the user — drives the attention icon.
|
||||
/// Whether any agent is blocked on the user — drives the attention badge
|
||||
/// on Windows/Linux (the macOS icon stays calm; the tooltip and menu
|
||||
/// carry agent status there).
|
||||
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
||||
pub(crate) fn attention(&self) -> bool {
|
||||
self.agents.iter().any(|a| a.status == AgentStatus::Waiting)
|
||||
}
|
||||
@@ -266,6 +271,7 @@ mod tests {
|
||||
fn attention_follows_waiting_and_tooltip_counts() {
|
||||
assert!(snapshot_with_agent(AgentStatus::Waiting).attention());
|
||||
assert!(!snapshot_with_agent(AgentStatus::Working).attention());
|
||||
assert!(!snapshot_with_agent(AgentStatus::Done).attention());
|
||||
assert_eq!(
|
||||
snapshot_with_agent(AgentStatus::Waiting).tooltip(),
|
||||
"tty7 — 1 waiting"
|
||||
|
||||
+25
-14
@@ -14,8 +14,10 @@ use tray_icon::{Icon, TrayIcon, TrayIconBuilder};
|
||||
|
||||
pub(super) struct Backend {
|
||||
tray: TrayIcon,
|
||||
/// The icon state currently shown, so a snapshot diff that doesn't flip
|
||||
/// attention skips the bitmap rebuild.
|
||||
/// Whether the amber badge is currently stamped, so a snapshot diff that
|
||||
/// doesn't flip attention skips the bitmap rebuild. macOS tracks nothing:
|
||||
/// its template glyph never changes (see `icon.rs`).
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
attention: bool,
|
||||
}
|
||||
|
||||
@@ -37,12 +39,15 @@ impl Backend {
|
||||
}
|
||||
}));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let img = icon::render()?;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let img = icon::render(false)?;
|
||||
let icon = Icon::from_rgba(img.data, img.width, img.height).ok()?;
|
||||
let tray = TrayIconBuilder::new()
|
||||
.with_icon(icon)
|
||||
// The calm glyph is a template on macOS (system recolors it for
|
||||
// the bar); a no-op on Windows.
|
||||
// The glyph is a template on macOS (system recolors it for the
|
||||
// bar, in both states); a no-op on Windows.
|
||||
.with_icon_as_template(true)
|
||||
.with_tooltip("tty7")
|
||||
.with_menu(Box::new(build_menu(&TraySnapshot::default())))
|
||||
@@ -59,24 +64,30 @@ impl Backend {
|
||||
};
|
||||
Some(Self {
|
||||
tray,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
attention: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Push a changed snapshot into the native item: menu always (it's what
|
||||
/// changed), icon only across an attention flip.
|
||||
/// changed); on Windows, the badge only across an attention flip. The
|
||||
/// macOS icon is a template image in every state — the system recolors it
|
||||
/// for the bar, and it never carries an attention mark (status lives in
|
||||
/// the tooltip and menu).
|
||||
pub(super) fn update(&mut self, snap: &TraySnapshot) {
|
||||
self.tray.set_menu(Some(Box::new(build_menu(snap))));
|
||||
let _ = self.tray.set_tooltip(Some(snap.tooltip()));
|
||||
let attention = snap.attention();
|
||||
if attention != self.attention {
|
||||
self.attention = attention;
|
||||
if let Some(img) = icon::render(attention)
|
||||
&& let Ok(icon) = Icon::from_rgba(img.data, img.width, img.height)
|
||||
{
|
||||
// Attention leaves template mode: the amber badge needs
|
||||
// its real color (macOS; the flag is a no-op on Windows).
|
||||
let _ = self.tray.set_icon_with_as_template(Some(icon), !attention);
|
||||
// Windows: flip the amber corner badge on the colored icon.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let attention = snap.attention();
|
||||
if attention != self.attention {
|
||||
self.attention = attention;
|
||||
if let Some(img) = icon::render(attention)
|
||||
&& let Ok(icon) = Icon::from_rgba(img.data, img.width, img.height)
|
||||
{
|
||||
let _ = self.tray.set_icon(Some(icon));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user