From f524091a5ba1dda61d9a9bdf914b957572efe9c4 Mon Sep 17 00:00:00 2001 From: naaiyy Date: Wed, 25 Mar 2026 02:52:18 +0100 Subject: [PATCH] Add native toolbar GPUI overlay support --- crates/gpui/src/platform.rs | 6 + crates/gpui/src/window.rs | 135 ++++++++++- .../gpui_macos/src/native_controls/panel.rs | 19 +- crates/gpui_macos/src/window.rs | 220 ++++++++++++++---- 4 files changed, 318 insertions(+), 62 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 450af5a..740faa3 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -647,6 +647,7 @@ pub struct PlatformNativeToolbarButtonItem { pub icon: Option, pub image_url: Option, pub image_circular: bool, + pub hosted_surface_view: Option<*mut std::ffi::c_void>, pub on_click: Option>, } @@ -711,6 +712,7 @@ pub struct PlatformNativeToolbarMenuButtonItem { pub icon: Option, pub image_url: Option, pub image_circular: bool, + pub hosted_surface_view: Option<*mut std::ffi::c_void>, pub shows_indicator: bool, pub items: Vec, pub on_select: Option>, @@ -1067,6 +1069,10 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { std::ptr::null_mut() } + fn raw_native_window_ptr(&self) -> *mut std::ffi::c_void { + std::ptr::null_mut() + } + fn native_controls(&self) -> Option<&dyn native_controls::PlatformNativeControls> { None } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 592d7fc..dc79215 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -711,6 +711,7 @@ pub struct NativeToolbarButton { icon: Option, image_url: Option, image_circular: bool, + hosted_view: Option, on_click: Option>, } @@ -724,6 +725,7 @@ impl NativeToolbarButton { icon: None, image_url: None, image_circular: false, + hosted_view: None, on_click: None, } } @@ -753,6 +755,15 @@ impl NativeToolbarButton { self } + /// Replaces the native icon/image with GPUI-rendered visual content while + /// preserving the native toolbar button's interaction behavior. + pub fn content_view(mut self, view: Entity) -> Self { + self.hosted_view = Some(AnyView::from(view)); + self.icon = None; + self.image_url = None; + self + } + /// Registers a callback invoked when this button is clicked. pub fn on_click( mut self, @@ -1179,6 +1190,7 @@ pub struct NativeToolbarMenuButton { icon: Option, image_url: Option, image_circular: bool, + hosted_view: Option, shows_indicator: bool, items: Vec, on_select: @@ -1199,6 +1211,7 @@ impl NativeToolbarMenuButton { icon: None, image_url: None, image_circular: false, + hosted_view: None, shows_indicator: true, items, on_select: None, @@ -1230,6 +1243,15 @@ impl NativeToolbarMenuButton { self } + /// Replaces the native icon/image with GPUI-rendered visual content while + /// preserving the native toolbar menu button's interaction behavior. + pub fn content_view(mut self, view: Entity) -> Self { + self.hosted_view = Some(AnyView::from(view)); + self.icon = None; + self.image_url = None; + self + } + /// Controls whether the dropdown chevron indicator is visible. pub fn shows_indicator(mut self, shows: bool) -> Self { self.shows_indicator = shows; @@ -1354,6 +1376,11 @@ pub struct NativeToolbar { items: Vec, } +struct NativeToolbarHostedSurface { + item_id: SharedString, + view: AnyView, +} + impl NativeToolbar { /// Creates a toolbar configuration with a stable identifier. pub fn new(identifier: impl Into) -> Self { @@ -1407,7 +1434,8 @@ impl NativeToolbar { self, next_frame_callbacks: Rc>>, invalidator: WindowInvalidator, - ) -> PlatformNativeToolbar { + ) -> (PlatformNativeToolbar, Vec) { + let mut hosted_surfaces = Vec::new(); let items = self .items .into_iter() @@ -1425,6 +1453,13 @@ impl NativeToolbar { ) }); + if let Some(hosted_view) = button.hosted_view { + hosted_surfaces.push(NativeToolbarHostedSurface { + item_id: button.id.clone(), + view: hosted_view, + }); + } + PlatformNativeToolbarItem::Button(PlatformNativeToolbarButtonItem { id: button.id, label: button.label, @@ -1432,6 +1467,7 @@ impl NativeToolbar { icon: button.icon, image_url: button.image_url, image_circular: button.image_circular, + hosted_surface_view: None, on_click, }) } @@ -1651,6 +1687,13 @@ impl NativeToolbar { }) } NativeToolbarItem::MenuButton(menu_button) => { + if let Some(hosted_view) = menu_button.hosted_view { + hosted_surfaces.push(NativeToolbarHostedSurface { + item_id: menu_button.id.clone(), + view: hosted_view, + }); + } + let btn_id = menu_button.id.clone(); let on_select = menu_button.on_select.map(|handler| { schedule_native_toolbar_callback( @@ -1671,6 +1714,7 @@ impl NativeToolbar { icon: menu_button.icon, image_url: menu_button.image_url, image_circular: menu_button.image_circular, + hosted_surface_view: None, shows_indicator: menu_button.shows_indicator, items: convert_toolbar_menu_items(&menu_button.items), on_select, @@ -1686,14 +1730,17 @@ impl NativeToolbar { }) .collect(); - PlatformNativeToolbar { - identifier: self.identifier, - title: self.title, - display_mode: self.display_mode.into(), - size_mode: self.size_mode.into(), - shows_baseline_separator: self.shows_baseline_separator, - items, - } + ( + PlatformNativeToolbar { + identifier: self.identifier, + title: self.title, + display_mode: self.display_mode.into(), + size_mode: self.size_mode.into(), + shows_baseline_separator: self.shows_baseline_separator, + items, + }, + hosted_surfaces, + ) } } @@ -3269,6 +3316,8 @@ pub struct Window { active_popover_surface: Option, #[cfg(target_os = "macos")] active_panel_surface: Option, + #[cfg(target_os = "macos")] + active_toolbar_surfaces: Vec, } #[derive(Clone, Debug, Default)] @@ -3798,6 +3847,8 @@ impl Window { active_popover_surface: None, #[cfg(target_os = "macos")] active_panel_surface: None, + #[cfg(target_os = "macos")] + active_toolbar_surfaces: Vec::new(), }) } @@ -4425,6 +4476,14 @@ impl Window { } } + #[cfg(target_os = "macos")] + fn clear_hosted_toolbar_surfaces(&mut self) { + let surface_ids: Vec<_> = self.active_toolbar_surfaces.drain(..).collect(); + for surface_id in surface_ids { + self.unregister_surface(surface_id); + } + } + #[cfg(target_os = "macos")] fn prepare_hosted_surface( &mut self, @@ -4485,8 +4544,47 @@ impl Window { /// On macOS this installs an `NSToolbar` with native items. /// On other platforms this is currently a no-op. pub fn set_native_toolbar(&mut self, toolbar: Option) { + #[cfg(target_os = "macos")] + self.clear_hosted_toolbar_surfaces(); + let toolbar = toolbar.map(|toolbar| { - toolbar.into_platform(self.next_frame_callbacks.clone(), self.invalidator.clone()) + let (mut platform_toolbar, hosted_surfaces) = + toolbar.into_platform(self.next_frame_callbacks.clone(), self.invalidator.clone()); + + #[cfg(target_os = "macos")] + for hosted_surface in hosted_surfaces { + let handle = self.register_surface(hosted_surface.view); + self.active_toolbar_surfaces.push(handle.id); + + if let Some(PlatformNativeToolbarItem::Button(button_item)) = platform_toolbar + .items + .iter_mut() + .find(|item| { + matches!( + item, + PlatformNativeToolbarItem::Button(button_item) + if button_item.id == hosted_surface.item_id + ) + }) + { + button_item.hosted_surface_view = Some(handle.native_view_ptr); + } else if let Some(PlatformNativeToolbarItem::MenuButton(menu_button_item)) = + platform_toolbar.items.iter_mut().find(|item| { + matches!( + item, + PlatformNativeToolbarItem::MenuButton(menu_button_item) + if menu_button_item.id == hosted_surface.item_id + ) + }) + { + menu_button_item.hosted_surface_view = Some(handle.native_view_ptr); + } + } + + #[cfg(not(target_os = "macos"))] + let _ = hosted_surfaces; + + platform_toolbar }); self.platform_window.set_native_toolbar(toolbar); } @@ -4644,6 +4742,23 @@ impl Window { } } + /// Returns the bounds of a native toolbar item in window coordinates. + /// + /// On macOS this resolves the installed `NSToolbarItem` by identifier and + /// returns its current frame in the content-view-local coordinate space. On + /// unsupported platforms this returns `None`. + pub fn native_toolbar_item_bounds(&self, item_id: &str) -> Option> { + self.platform_window + .native_controls() + .and_then(|native_controls| { + let native_window = self.platform_window.raw_native_window_ptr(); + if native_window.is_null() { + return None; + } + native_controls.get_toolbar_item_frame(native_window, item_id) + }) + } + /// Returns the platform's native controls implementation for creating /// and updating native UI elements (buttons, text fields, etc.). pub fn native_controls(&self) -> &dyn crate::platform::native_controls::PlatformNativeControls { diff --git a/crates/gpui_macos/src/native_controls/panel.rs b/crates/gpui_macos/src/native_controls/panel.rs index 36a706e..894832f 100644 --- a/crates/gpui_macos/src/native_controls/panel.rs +++ b/crates/gpui_macos/src/native_controls/panel.rs @@ -324,7 +324,7 @@ pub(crate) unsafe fn is_native_panel_visible(panel: id) -> bool { } } -/// Gets the screen frame of a toolbar item by its identifier. +/// Gets the content-view-local frame of a toolbar item by its identifier. /// Returns None if the toolbar or item is not found. pub(crate) unsafe fn get_toolbar_item_screen_frame( window: id, @@ -361,8 +361,21 @@ pub(crate) unsafe fn get_toolbar_item_screen_frame( if view_window == nil { return None; } - let screen_rect: NSRect = msg_send![view_window, convertRectToScreen: window_rect]; - return Some(screen_rect); + let content_view: id = msg_send![view_window, contentView]; + if content_view == nil { + return None; + } + let content_rect: NSRect = + msg_send![content_view, convertRect: window_rect fromView: nil]; + let content_bounds: NSRect = msg_send![content_view, bounds]; + let flipped_rect = NSRect::new( + NSPoint::new( + content_rect.origin.x, + content_bounds.size.height - content_rect.origin.y - content_rect.size.height, + ), + content_rect.size, + ); + return Some(flipped_rect); } } diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index d84f70c..a3d5849 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -457,7 +457,7 @@ enum ToolbarNativeResource { SegmentedControl { control: id, target: *mut c_void }, PopUpButton { popup: id, target: *mut c_void }, ComboBox { combo: id, delegate: *mut c_void }, - MenuButton { target: *mut c_void }, + MenuButton { button: Option, target: *mut c_void }, } struct ToolbarState { @@ -517,8 +517,11 @@ impl MacToolbarState { crate::native_controls::release_native_combo_box_delegate(delegate); crate::native_controls::release_native_combo_box(combo); } - ToolbarNativeResource::MenuButton { target } => { + ToolbarNativeResource::MenuButton { button, target } => { crate::native_controls::release_native_menu_button_target(target); + if let Some(button) = button { + crate::native_controls::release_native_menu_button(button); + } } } } @@ -2633,6 +2636,10 @@ impl PlatformWindow for MacWindow { self.0.lock().native_view.as_ptr() as *mut c_void } + fn raw_native_window_ptr(&self) -> *mut c_void { + self.0.lock().native_window as *mut c_void + } + fn native_controls(&self) -> Option<&dyn gpui::native_controls::PlatformNativeControls> { Some(&MAC_NATIVE_CONTROLS) } @@ -4400,10 +4407,16 @@ unsafe fn create_toolbar_button_item( let icon = item.icon.clone(); let image_url = item.image_url.clone(); let image_circular = item.image_circular; + let hosted_surface_view = item.hosted_surface_view; let toolbar_item: id = msg_send![class!(NSToolbarItem), alloc]; let toolbar_item: id = msg_send![toolbar_item, initWithItemIdentifier: identifier]; - let button = crate::native_controls::create_native_button(label.as_ref()); + let button_title = if hosted_surface_view.is_some() { + "" + } else { + label.as_ref() + }; + let button = crate::native_controls::create_native_button(button_title); let state_ptr: *mut c_void = *this.get_ivar(TOOLBAR_STATE_IVAR); let callback_identifier = identifier_string.to_owned(); @@ -4421,33 +4434,74 @@ unsafe fn create_toolbar_button_item( crate::native_controls::set_native_view_tooltip(button, tool_tip.as_ref()); } - // Set SF Symbol icon on both the toolbar item and the button view. - // The toolbar item image is used by the customization panel, - // while the button image is what's actually displayed (since setView: overrides). - if let Some(icon) = icon.as_ref() { - let symbol_name = ns_string(icon.as_ref()); - let image: id = msg_send![ - class!(NSImage), - imageWithSystemSymbolName: symbol_name - accessibilityDescription: nil - ]; - if image != nil { - let _: () = msg_send![toolbar_item, setImage: image]; + let host_view = if let Some(surface_view) = hosted_surface_view { + let surface_view = surface_view as id; + crate::native_controls::set_native_button_bezel_style(button, 12); + crate::native_controls::set_native_button_bordered(button, false); + crate::native_controls::set_native_button_shows_border_on_hover(button, false); + + let mut size: NSSize = if label.is_empty() { + msg_send![button, fittingSize] + } else { + let sizing_button = crate::native_controls::create_native_button(label.as_ref()); + let size: NSSize = msg_send![sizing_button, fittingSize]; + crate::native_controls::release_native_button(sizing_button); + size + }; + size.width += 22.0; + let frame = NSRect::new(NSPoint::new(0.0, 0.0), size); + + let container: id = msg_send![class!(NSView), alloc]; + let container: id = msg_send![container, initWithFrame: frame]; + let _: () = msg_send![container, setAutoresizingMask: 0u64]; + + let _: () = msg_send![surface_view, setFrame: frame]; + let _: () = msg_send![surface_view, setAutoresizingMask: 18u64]; + let _: () = msg_send![container, addSubview: surface_view]; + let layer: id = msg_send![surface_view, layer]; + if layer != nil { + let _: () = msg_send![layer, setOpaque: 0i8]; } - let image_only = label.is_empty(); - crate::native_controls::set_native_button_sf_symbol(button, icon.as_ref(), image_only); - } - // Load image from URL asynchronously if provided - if let Some(url_str) = image_url.as_ref() { - load_toolbar_image_from_url(toolbar_item, url_str, image_circular); + let _: () = msg_send![button, setFrame: frame]; + let _: () = msg_send![button, setAutoresizingMask: 18u64]; + let _: () = msg_send![container, addSubview: button]; + + let _: () = msg_send![container, autorelease]; + container + } else { + if let Some(icon) = icon.as_ref() { + let symbol_name = ns_string(icon.as_ref()); + let image: id = msg_send![ + class!(NSImage), + imageWithSystemSymbolName: symbol_name + accessibilityDescription: nil + ]; + if image != nil { + let _: () = msg_send![toolbar_item, setImage: image]; + } + let image_only = label.is_empty(); + crate::native_controls::set_native_button_sf_symbol( + button, + icon.as_ref(), + image_only, + ); + } + + button + }; + + if hosted_surface_view.is_none() { + if let Some(url_str) = image_url.as_ref() { + load_toolbar_image_from_url(toolbar_item, url_str, image_circular); + } } let _: () = msg_send![toolbar_item, setLabel: ns_string(label.as_ref())]; - let size: NSSize = msg_send![button, fittingSize]; + let size: NSSize = msg_send![host_view, fittingSize]; let _: () = msg_send![toolbar_item, setMinSize: size]; let _: () = msg_send![toolbar_item, setMaxSize: size]; - let _: () = msg_send![toolbar_item, setView: button]; + let _: () = msg_send![toolbar_item, setView: host_view]; state .resources @@ -4842,12 +4896,10 @@ unsafe fn create_toolbar_menu_button_item( let icon = item.icon.clone(); let image_url = item.image_url.clone(); let image_circular = item.image_circular; + let hosted_surface_view = item.hosted_surface_view; let shows_indicator = item.shows_indicator; let native_menu_items = convert_platform_menu_items_to_native(&item.items); - let toolbar_item: id = msg_send![class!(NSMenuToolbarItem), alloc]; - let toolbar_item: id = msg_send![toolbar_item, initWithItemIdentifier: identifier]; - let state_ptr: *mut c_void = *this.get_ivar(TOOLBAR_STATE_IVAR); let callback_identifier = identifier_string.to_owned(); @@ -4861,37 +4913,107 @@ unsafe fn create_toolbar_menu_button_item( } })); - let (menu, target_ptr) = - crate::native_controls::create_native_menu_target(&native_menu_items, on_select); + if let Some(surface_view) = hosted_surface_view { + let toolbar_item: id = msg_send![class!(NSToolbarItem), alloc]; + let toolbar_item: id = msg_send![toolbar_item, initWithItemIdentifier: identifier]; + + let button = crate::native_controls::create_native_menu_button(""); + crate::native_controls::set_native_button_bezel_style(button, 12); + crate::native_controls::set_native_button_bordered(button, false); + crate::native_controls::set_native_button_shows_border_on_hover(button, false); + let target_ptr = crate::native_controls::set_native_menu_button_items( + button, + &native_menu_items, + on_select, + ); - let _: () = msg_send![toolbar_item, setMenu: menu]; - // Release our extra retain - NSMenuToolbarItem retains the menu internally - let _: () = msg_send![menu, release]; + if let Some(tool_tip) = tool_tip.as_ref() { + crate::native_controls::set_native_view_tooltip(button, tool_tip.as_ref()); + } - let _: () = msg_send![toolbar_item, setLabel: ns_string(label.as_ref())]; - let _: () = msg_send![toolbar_item, setShowsIndicator: shows_indicator as BOOL]; + let surface_view = surface_view as id; + let mut size: NSSize = if label.is_empty() { + msg_send![button, fittingSize] + } else { + let sizing_button = + crate::native_controls::create_native_menu_button(label.as_ref()); + let size: NSSize = msg_send![sizing_button, fittingSize]; + crate::native_controls::release_native_menu_button(sizing_button); + size + }; + size.width += 28.0; + let frame = NSRect::new(NSPoint::new(0.0, 0.0), size); + + let container: id = msg_send![class!(NSView), alloc]; + let container: id = msg_send![container, initWithFrame: frame]; + let _: () = msg_send![container, setAutoresizingMask: 0u64]; + + let _: () = msg_send![surface_view, setFrame: frame]; + let _: () = msg_send![surface_view, setAutoresizingMask: 18u64]; + let _: () = msg_send![container, addSubview: surface_view]; + let layer: id = msg_send![surface_view, layer]; + if layer != nil { + let _: () = msg_send![layer, setOpaque: 0i8]; + } - if let Some(tool_tip) = tool_tip.as_ref() { - let _: () = msg_send![toolbar_item, setToolTip: ns_string(tool_tip.as_ref())]; - } + let _: () = msg_send![button, setFrame: frame]; + let _: () = msg_send![button, setAutoresizingMask: 18u64]; + let _: () = msg_send![container, addSubview: button]; - if let Some(icon) = icon.as_ref() { - let symbol_name = ns_string(icon.as_ref()); - let image: id = msg_send![class!(NSImage), imageWithSystemSymbolName: symbol_name accessibilityDescription: nil]; - if image != nil { - let _: () = msg_send![toolbar_item, setImage: image]; + let _: () = msg_send![toolbar_item, setLabel: ns_string(label.as_ref())]; + let fitted_size: NSSize = msg_send![container, fittingSize]; + let _: () = msg_send![toolbar_item, setMinSize: fitted_size]; + let _: () = msg_send![toolbar_item, setMaxSize: fitted_size]; + let _: () = msg_send![toolbar_item, setView: container]; + let _: () = msg_send![container, autorelease]; + + state.resources.push(ToolbarNativeResource::MenuButton { + button: Some(button), + target: target_ptr, + }); + + msg_send![toolbar_item, autorelease] + } else { + let toolbar_item: id = msg_send![class!(NSMenuToolbarItem), alloc]; + let toolbar_item: id = msg_send![toolbar_item, initWithItemIdentifier: identifier]; + + let (menu, target_ptr) = + crate::native_controls::create_native_menu_target(&native_menu_items, on_select); + + let _: () = msg_send![toolbar_item, setMenu: menu]; + // Release our extra retain - NSMenuToolbarItem retains the menu internally + let _: () = msg_send![menu, release]; + + let _: () = msg_send![toolbar_item, setLabel: ns_string(label.as_ref())]; + let _: () = msg_send![toolbar_item, setShowsIndicator: shows_indicator as BOOL]; + + if let Some(tool_tip) = tool_tip.as_ref() { + let _: () = msg_send![toolbar_item, setToolTip: ns_string(tool_tip.as_ref())]; } - } - if let Some(url_str) = image_url.as_ref() { - load_toolbar_image_from_url(toolbar_item, url_str, image_circular); - } + if let Some(icon) = icon.as_ref() { + let symbol_name = ns_string(icon.as_ref()); + let image: id = msg_send![ + class!(NSImage), + imageWithSystemSymbolName: symbol_name + accessibilityDescription: nil + ]; + if image != nil { + let _: () = msg_send![toolbar_item, setImage: image]; + } + } - state - .resources - .push(ToolbarNativeResource::MenuButton { target: target_ptr }); + if let Some(url_str) = image_url.as_ref() { + load_toolbar_image_from_url(toolbar_item, url_str, image_circular); + } - msg_send![toolbar_item, autorelease] + state.resources.push(ToolbarNativeResource::MenuButton { + button: None, + target: target_ptr, + }); + + msg_send![toolbar_item, autorelease] + } } }