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
This commit is contained in:
Mayank Verma authored and GitHub committed 2026-07-31 06:38:08 +00:00
1 parent 7155ded145
commit 938f8d7454
7 files changed
+116 -55

No files matched your search

+1 -1
View File
@@ -865,7 +865,7 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
#[cfg(target_os = "macos")]
fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
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<dyn FnMut()>) {}
fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
+21 -4
View File
@@ -1076,6 +1076,8 @@ pub struct Window {
pub(crate) removed: bool,
pub(crate) platform_window: Box<dyn PlatformWindow>,
display_id: Option<DisplayId>,
is_resizable: bool,
is_minimizable: bool,
sprite_atlas: Arc<dyn PlatformAtlas>,
text_system: Arc<WindowTextSystem>,
text_rendering_mode: Rc<Cell<TextRenderingMode>>,
@@ -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.
+13 -5
View File
@@ -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);
}
}
}
}
@@ -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);
@@ -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(),
@@ -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())
}
+33 -29
View File
@@ -11125,6 +11125,7 @@ pub fn client_side_decorations(
) -> Stateful<Div> {
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::<GlobalResizeEdge>();
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::<GlobalResizeEdge>();
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,
})
}