From 938f8d7454998d71da2dcb8aa745e802fa2614ba Mon Sep 17 00:00:00 2001 From: Mayank Verma Date: Fri, 31 Jul 2026 12:08:08 +0530 Subject: [PATCH] gpui: Add resizable and minimizable state to Window (#61473) ### Objective PR #36859 made native macOS and Windows controls respect `is_resizable` and `is_minimizable`, but custom window decorations cannot access those values from `Window`. Linux custom controls and client-side resize interactions remain active, macOS custom titlebar double-click does not check either option and custom Windows caption buttons still appear enabled even though the backend blocks their actions. ### Solution Expose `is_resizable` and `is_minimizable` on `Window`. Use them to disable custom window controls and titlebar actions and omit client-side resize cursors and hitboxes for non-resizable windows. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/window.rs | 25 ++++++-- crates/gpui_macos/src/window.rs | 18 ++++-- .../src/platform_title_bar.rs | 19 +++--- .../src/platforms/platform_linux.rs | 28 +++++++-- .../src/platforms/platform_windows.rs | 17 ++++- crates/workspace/src/workspace.rs | 62 ++++++++++--------- 7 files changed, 116 insertions(+), 55 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 5f8c63f932..9d4146a31e 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -865,7 +865,7 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { #[cfg(target_os = "macos")] fn set_traffic_light_position(&self, _position: Point) {} fn show_character_palette(&self) {} - fn titlebar_double_click(&self) {} + fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {} fn on_move_tab_to_new_window(&self, _callback: Box) {} fn on_merge_all_windows(&self, _callback: Box) {} fn on_select_previous_tab(&self, _callback: Box) {} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index b79c8947a2..3e8563c01b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1076,6 +1076,8 @@ pub struct Window { pub(crate) removed: bool, pub(crate) platform_window: Box, display_id: Option, + is_resizable: bool, + is_minimizable: bool, sprite_atlas: Arc, text_system: Arc, text_rendering_mode: Rc>, @@ -1829,6 +1831,8 @@ impl Window { removed: false, platform_window, display_id, + is_resizable, + is_minimizable, sprite_atlas, text_system, text_rendering_mode: cx.text_rendering_mode.clone(), @@ -2137,9 +2141,11 @@ impl Window { self.platform_window.set_exclusive_edge(edge); } - /// Start a window resize operation (Wayland) + /// Start an interactive window resize operation if this window is resizable. pub fn start_window_resize(&self, edge: ResizeEdge) { - self.platform_window.start_window_resize(edge); + if self.is_resizable { + self.platform_window.start_window_resize(edge); + } } /// Linux (wayland) only: Set the window's input region, the area that receives pointer @@ -2524,7 +2530,17 @@ impl Window { self.platform_window.window_decorations() } - /// Returns which window controls are currently visible (Wayland) + /// Returns whether this window is resizable. + pub fn is_resizable(&self) -> bool { + self.is_resizable + } + + /// Returns whether this window is minimizable. + pub fn is_minimizable(&self) -> bool { + self.is_minimizable + } + + /// Returns the controls supported by the platform. pub fn window_controls(&self) -> WindowControls { self.platform_window.window_controls() } @@ -5878,7 +5894,8 @@ impl Window { /// Perform titlebar double-click action. /// This is macOS specific. pub fn titlebar_double_click(&self) { - self.platform_window.titlebar_double_click(); + self.platform_window + .titlebar_double_click(self.is_resizable, self.is_minimizable); } /// Gets the window's title at the platform level. diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 6b8a8a6be3..99c9544736 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -1784,7 +1784,7 @@ impl PlatformWindow for MacWindow { .detach() } - fn titlebar_double_click(&self) { + fn titlebar_double_click(&self, is_resizable: bool, is_minimizable: bool) { let this = self.0.lock(); let window = this.native_window; let closed = this.closed.clone(); @@ -1814,17 +1814,25 @@ impl PlatformWindow for MacWindow { // "Do Nothing" selected, so do no action } "Minimize" => { - window.miniaturize_(nil); + if is_minimizable { + window.miniaturize_(nil); + } } "Maximize" => { - window.zoom_(nil); + if is_resizable { + window.zoom_(nil); + } } "Fill" => { // There is no documented API for "Fill" action, so we'll just zoom the window - window.zoom_(nil); + if is_resizable { + window.zoom_(nil); + } } _ => { - window.zoom_(nil); + if is_resizable { + window.zoom_(nil); + } } } } diff --git a/crates/platform_title_bar/src/platform_title_bar.rs b/crates/platform_title_bar/src/platform_title_bar.rs index 8b7304e19f..d0207fd274 100644 --- a/crates/platform_title_bar/src/platform_title_bar.rs +++ b/crates/platform_title_bar/src/platform_title_bar.rs @@ -229,13 +229,18 @@ impl Render for PlatformTitleBar { } }) }) - .when(self.platform_style == PlatformStyle::Linux, |this| { - this.on_click(|event, window, _| { - if event.click_count() == 2 { - window.zoom_window(); - } - }) - }) + .when( + self.platform_style == PlatformStyle::Linux + && supported_controls.maximize + && window.is_resizable(), + |this| { + this.on_click(|event, window, _| { + if event.click_count() == 2 { + window.zoom_window(); + } + }) + }, + ) }) .map(|this| { let show_left_controls = !(sidebar.open && sidebar.side == SidebarSide::Left); diff --git a/crates/platform_title_bar/src/platforms/platform_linux.rs b/crates/platform_title_bar/src/platforms/platform_linux.rs index 8dd6c6f678..5ebf2dc60b 100644 --- a/crates/platform_title_bar/src/platforms/platform_linux.rs +++ b/crates/platform_title_bar/src/platforms/platform_linux.rs @@ -205,29 +205,45 @@ impl WindowControl { } impl RenderOnce for WindowControl { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let enabled = match self.icon { + WindowControlType::Minimize => window.is_minimizable(), + WindowControlType::Restore | WindowControlType::Maximize => window.is_resizable(), + WindowControlType::Close => true, + }; let icon = svg() .size_4() .flex_none() .path(self.icon.icon().path()) - .text_color(self.style.icon) - .group_hover("", |this| this.text_color(self.style.icon_hover)); + .text_color(if enabled { + self.style.icon + } else { + cx.theme().colors().icon_disabled + }) + .when(enabled, |this| { + this.group_hover("", |this| this.text_color(self.style.icon_hover)) + }); h_flex() .id(self.id) .group("") - .cursor_pointer() .justify_center() .content_center() .rounded_2xl() .w_5() .h_5() - .hover(|this| this.bg(self.style.background_hover)) - .active(|this| this.bg(self.style.background_hover)) + .when(enabled, |this| { + this.cursor_pointer() + .hover(|this| this.bg(self.style.background_hover)) + .active(|this| this.bg(self.style.background_hover)) + }) .child(icon) .on_mouse_move(|_, _, cx| cx.stop_propagation()) .on_click(move |_, window, cx| { cx.stop_propagation(); + if !enabled { + return; + } match self.icon { WindowControlType::Minimize => window.minimize_window(), WindowControlType::Restore => window.zoom_window(), diff --git a/crates/platform_title_bar/src/platforms/platform_windows.rs b/crates/platform_title_bar/src/platforms/platform_windows.rs index 4b882c8275..9ea9917cd7 100644 --- a/crates/platform_title_bar/src/platforms/platform_windows.rs +++ b/crates/platform_title_bar/src/platforms/platform_windows.rs @@ -95,7 +95,12 @@ impl WindowsCaptionButton { } impl RenderOnce for WindowsCaptionButton { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let enabled = match &self { + Self::Minimize => window.is_minimizable(), + Self::Restore | Self::Maximize => window.is_resizable(), + Self::Close => true, + }; let (hover_bg, hover_fg, active_bg, active_fg) = match self { Self::Close => { let color: Hsla = Rgba { @@ -129,8 +134,14 @@ impl RenderOnce for WindowsCaptionButton { .w(px(36.)) .h_full() .text_size(px(10.0)) - .hover(|style| style.bg(hover_bg).text_color(hover_fg)) - .active(|style| style.bg(active_bg).text_color(active_fg)) + .when(!enabled, |style| { + style.text_color(cx.theme().colors().text_disabled) + }) + .when(enabled, |style| { + style + .hover(|style| style.bg(hover_bg).text_color(hover_fg)) + .active(|style| style.bg(active_bg).text_color(active_fg)) + }) .window_control_area(self.control_area()) .child(self.icon()) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044670f17d..2e287b1118 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -11125,6 +11125,7 @@ pub fn client_side_decorations( ) -> Stateful
{ const BORDER_SIZE: Pixels = px(1.0); let decorations = window.window_decorations(); + let is_resizable = window.is_resizable(); let tiling = match decorations { Decorations::Server => Tiling::default(), Decorations::Client { tiling } => tiling, @@ -11157,38 +11158,40 @@ pub fn client_side_decorations( .when(!tiling.right, |div| { div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW) }) - .on_mouse_move(move |e, window, cx| { - let size = window.window_bounds().get_bounds().size; - let pos = e.position; + .when(is_resizable, |div| { + div.on_mouse_move(move |e, window, cx| { + let size = window.window_bounds().get_bounds().size; + let pos = e.position; - let new_edge = - resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling); + let new_edge = + resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling); - let edge = cx.try_global::(); - if new_edge != edge.map(|edge| edge.0) { - window - .window_handle() - .update(cx, |workspace, _, cx| { - cx.notify(workspace.entity_id()); - }) - .ok(); - } - }) - .on_mouse_down(MouseButton::Left, move |e, window, _| { - let size = window.window_bounds().get_bounds().size; - let pos = e.position; + let edge = cx.try_global::(); + if new_edge != edge.map(|edge| edge.0) { + window + .window_handle() + .update(cx, |workspace, _, cx| { + cx.notify(workspace.entity_id()); + }) + .ok(); + } + }) + .on_mouse_down(MouseButton::Left, move |e, window, _| { + let size = window.window_bounds().get_bounds().size; + let pos = e.position; - let edge = match resize_edge( - pos, - theme::CLIENT_SIDE_DECORATION_SHADOW, - size, - tiling, - ) { - Some(value) => value, - None => return, - }; + let edge = match resize_edge( + pos, + theme::CLIENT_SIDE_DECORATION_SHADOW, + size, + tiling, + ) { + Some(value) => value, + None => return, + }; - window.start_window_resize(edge); + window.start_window_resize(edge); + }) }), }) .size_full() @@ -11228,7 +11231,7 @@ pub fn client_side_decorations( ) .map(|div| match decorations { Decorations::Server => div, - Decorations::Client { tiling, .. } => div.child( + Decorations::Client { tiling, .. } if is_resizable => div.child( canvas( |_bounds, window, _| { window.insert_hitbox( @@ -11268,6 +11271,7 @@ pub fn client_side_decorations( .size_full() .absolute(), ), + Decorations::Client { .. } => div, }) }