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:
1 parent
7155ded145
commit
938f8d7454
7 files changed
+116
-55
No files matched your search
@@ -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()>) {}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user