From 452fb759278918df7375203f49b5add71681cbe1 Mon Sep 17 00:00:00 2001 From: Cyberistic Date: Sat, 31 Oct 2026 00:13:13 +0300 Subject: [PATCH 1/2] gpui: set and clear the dock icon's badge Adds `App::set_dock_badge`, which writes the label macOS draws on the dock icon and clears it with `None`. Only macOS implements it; the platform method is a no-op by default. The label lives on the dock tile, so a menu-bar-only app (`ActivationPolicy::Accessory`) has no icon to badge. Off the main thread the call logs and does nothing. Co-Authored-By: Claude Opus 5.5 --- crates/gpui/src/app.rs | 21 +++++++++++++++++++++ crates/gpui/src/app/test_context.rs | 6 ++++++ crates/gpui/src/platform.rs | 5 +++++ crates/gpui/src/platform/test/platform.rs | 11 +++++++++++ crates/gpui_macos/Cargo.toml | 2 +- crates/gpui_macos/src/dock.rs | 21 +++++++++++++++++++++ crates/gpui_macos/src/gpui_macos.rs | 1 + crates/gpui_macos/src/platform.rs | 4 ++++ 8 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 crates/gpui_macos/src/dock.rs diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a2c2affacd1ea1..0f10710588e6c0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1317,6 +1317,16 @@ impl App { self.platform.unhide_other_apps(); } + /// Sets the label drawn on the application's dock icon, or clears it with + /// `None`. + /// + /// Only macOS implements it; elsewhere the call does nothing. A + /// menu-bar-only app (`ActivationPolicy::Accessory`) has no dock icon, so + /// the label has nowhere to appear. + pub fn set_dock_badge(&self, label: Option<&str>) { + self.platform.set_dock_badge(label); + } + /// Returns the list of currently active displays. pub fn displays(&self) -> Vec> { self.platform.displays() @@ -3122,6 +3132,17 @@ mod test { } } + /// The badge belongs to the platform: `App` must forward it, and `None` + /// must clear it rather than leave the previous label in place. + #[gpui::test] + fn set_dock_badge_forwards_to_the_platform(cx: &mut TestAppContext) { + cx.update(|cx| cx.set_dock_badge(Some("3"))); + assert_eq!(cx.dock_badge().as_deref(), Some("3")); + + cx.update(|cx| cx.set_dock_badge(None)); + assert_eq!(cx.dock_badge(), None); + } + #[gpui::test] fn async_app_refresh_flushes_refresh_effect(cx: &mut TestAppContext) { let render_count = Rc::new(Cell::new(0)); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 1dceee09d746ed..73dff2b485a9c0 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -215,6 +215,12 @@ impl TestAppContext { f(&cx) } + /// The dock badge the application last set on the platform. + #[cfg(test)] + pub(crate) fn dock_badge(&self) -> Option { + self.test_platform.dock_badge() + } + /// Adds a new window. The Window will always be backed by a `TestWindow` which /// can be retrieved with `self.test_window(handle)` pub fn add_window(&mut self, build_window: F) -> WindowHandle diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index e37ab998ddda54..81e9710ec346ee 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -142,6 +142,11 @@ pub trait Platform: 'static { fn hide_other_apps(&self); fn unhide_other_apps(&self); + /// Sets the label drawn on the application's dock icon, or clears it. + /// + /// Only macOS implements it; the default is a no-op. + fn set_dock_badge(&self, _label: Option<&str>) {} + fn displays(&self) -> Vec>; fn primary_display(&self) -> Option>; fn active_window(&self) -> Option; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 86c2297d842305..b8b51fee84ca74 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -34,6 +34,7 @@ pub(crate) struct TestPlatform { screen_capture_sources: RefCell>, pub opened_url: RefCell>, pub(crate) system_notifications: RefCell, + dock_badge: RefCell>, pub text_system: Arc, pub expect_restart: RefCell, Vec)>>>, @@ -146,11 +147,17 @@ impl TestPlatform { weak: weak.clone(), opened_url: Default::default(), system_notifications: Default::default(), + dock_badge: Default::default(), text_system, headless_renderer_factory, }) } + #[cfg(test)] + pub(crate) fn dock_badge(&self) -> Option { + self.dock_badge.borrow().clone() + } + pub(crate) fn simulate_new_path_selection( &self, select_path: impl FnOnce(&std::path::Path) -> Option, @@ -502,6 +509,10 @@ impl Platform for TestPlatform { self.system_notifications.borrow_mut().response_callback = Some(callback); } + fn set_dock_badge(&self, label: Option<&str>) { + *self.dock_badge.borrow_mut() = label.map(str::to_string); + } + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} diff --git a/crates/gpui_macos/Cargo.toml b/crates/gpui_macos/Cargo.toml index ff10902a37c69a..b88878ddfd51b0 100644 --- a/crates/gpui_macos/Cargo.toml +++ b/crates/gpui_macos/Cargo.toml @@ -51,7 +51,7 @@ media.workspace = true metal.workspace = true objc.workspace = true objc2.workspace = true -objc2-app-kit.workspace = true +objc2-app-kit = { workspace = true, features = ["NSApplication", "NSDockTile"] } objc2-foundation.workspace = true objc2-user-notifications.workspace = true parking_lot.workspace = true diff --git a/crates/gpui_macos/src/dock.rs b/crates/gpui_macos/src/dock.rs new file mode 100644 index 00000000000000..997a1ed0c6de19 --- /dev/null +++ b/crates/gpui_macos/src/dock.rs @@ -0,0 +1,21 @@ +//! The dock icon's badge, the one label macOS draws outside the app's windows. +//! +//! The badge belongs to the dock tile, so it only exists while the app has a +//! dock icon: a menu-bar-only app (activation policy `Accessory`) has nothing +//! to draw it on and this becomes a no-op. + +use objc2::MainThreadMarker; +use objc2_app_kit::NSApplication; +use objc2_foundation::NSString; + +/// Sets the dock icon's badge, or clears it with `None`. +pub(crate) fn set_dock_badge(label: Option<&str>) { + let Some(mtm) = MainThreadMarker::new() else { + log::warn!("the dock badge must be set on the main thread"); + return; + }; + let label = label.map(NSString::from_str); + NSApplication::sharedApplication(mtm) + .dockTile() + .setBadgeLabel(label.as_deref()); +} diff --git a/crates/gpui_macos/src/gpui_macos.rs b/crates/gpui_macos/src/gpui_macos.rs index 41984dc9ce009c..819fa84f0d7781 100644 --- a/crates/gpui_macos/src/gpui_macos.rs +++ b/crates/gpui_macos/src/gpui_macos.rs @@ -7,6 +7,7 @@ mod dispatcher; mod display; mod display_link; +mod dock; mod events; mod keyboard; mod pasteboard; diff --git a/crates/gpui_macos/src/platform.rs b/crates/gpui_macos/src/platform.rs index 91dba604977357..6b9013d7d0f8f4 100644 --- a/crates/gpui_macos/src/platform.rs +++ b/crates/gpui_macos/src/platform.rs @@ -837,6 +837,10 @@ impl Platform for MacPlatform { } } + fn set_dock_badge(&self, label: Option<&str>) { + crate::dock::set_dock_badge(label); + } + fn primary_display(&self) -> Option> { Some(Rc::new(MacDisplay::primary())) } From 9b906c312e2769994b6368ed02666745d7d0d51e Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Mon, 2 Nov 2026 10:23:49 +0100 Subject: [PATCH 2/2] gpui: remove fake dock badge test state Remove the forwarding-only test and its TestPlatform storage and getters. The fake platform could pass even if the macOS dock tile implementation were broken; retain the default no-op for the test platform. Validated the current gpuix base with normal Metal shaders: cargo check for all GPUI and macOS targets, cargo build for both libraries, and 290 passing library tests. Session: ses_e64752538ffeiplE1EUF1UCkId Discord: https://discord.com/channels/1422625037164351591/1566742541918019655 --- crates/gpui/src/app.rs | 11 ----------- crates/gpui/src/app/test_context.rs | 6 ------ crates/gpui/src/platform/test/platform.rs | 11 ----------- 3 files changed, 28 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0f10710588e6c0..79fab690dcbd53 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3132,17 +3132,6 @@ mod test { } } - /// The badge belongs to the platform: `App` must forward it, and `None` - /// must clear it rather than leave the previous label in place. - #[gpui::test] - fn set_dock_badge_forwards_to_the_platform(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_dock_badge(Some("3"))); - assert_eq!(cx.dock_badge().as_deref(), Some("3")); - - cx.update(|cx| cx.set_dock_badge(None)); - assert_eq!(cx.dock_badge(), None); - } - #[gpui::test] fn async_app_refresh_flushes_refresh_effect(cx: &mut TestAppContext) { let render_count = Rc::new(Cell::new(0)); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 73dff2b485a9c0..1dceee09d746ed 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -215,12 +215,6 @@ impl TestAppContext { f(&cx) } - /// The dock badge the application last set on the platform. - #[cfg(test)] - pub(crate) fn dock_badge(&self) -> Option { - self.test_platform.dock_badge() - } - /// Adds a new window. The Window will always be backed by a `TestWindow` which /// can be retrieved with `self.test_window(handle)` pub fn add_window(&mut self, build_window: F) -> WindowHandle diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index b8b51fee84ca74..86c2297d842305 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -34,7 +34,6 @@ pub(crate) struct TestPlatform { screen_capture_sources: RefCell>, pub opened_url: RefCell>, pub(crate) system_notifications: RefCell, - dock_badge: RefCell>, pub text_system: Arc, pub expect_restart: RefCell, Vec)>>>, @@ -147,17 +146,11 @@ impl TestPlatform { weak: weak.clone(), opened_url: Default::default(), system_notifications: Default::default(), - dock_badge: Default::default(), text_system, headless_renderer_factory, }) } - #[cfg(test)] - pub(crate) fn dock_badge(&self) -> Option { - self.dock_badge.borrow().clone() - } - pub(crate) fn simulate_new_path_selection( &self, select_path: impl FnOnce(&std::path::Path) -> Option, @@ -509,10 +502,6 @@ impl Platform for TestPlatform { self.system_notifications.borrow_mut().response_callback = Some(callback); } - fn set_dock_badge(&self, label: Option<&str>) { - *self.dock_badge.borrow_mut() = label.map(str::to_string); - } - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {}