diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index fa6fcace40f376..89e02aacbcc46e 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -197,6 +197,10 @@ path = "examples/input.rs" name = "on_window_close_quit" path = "examples/on_window_close_quit.rs" +[[example]] +name = "window_movable" +path = "examples/window_movable.rs" + [[example]] name = "opacity" path = "examples/opacity.rs" diff --git a/crates/gpui/examples/window_movable.rs b/crates/gpui/examples/window_movable.rs new file mode 100644 index 00000000000000..587a2dbfea55cc --- /dev/null +++ b/crates/gpui/examples/window_movable.rs @@ -0,0 +1,125 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +use gpui::{ + App, Bounds, Context, FocusHandle, Window, WindowBounds, WindowOptions, div, prelude::*, px, + rgb, size, +}; +use gpui::{SharedString, TitlebarOptions}; +use gpui_platform::application; + +struct ExampleWindow { + label: SharedString, + focus_handle: FocusHandle, +} + +impl Render for ExampleWindow { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .track_focus(&self.focus_handle) + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x2e2e2e)) + .size_full() + .justify_center() + .items_center() + .p_8() + .text_lg() + .text_color(rgb(0xffffff)) + .child(self.label.clone()) + .child( + div() + .text_sm() + .text_color(rgb(0xb0b0b0)) + .child("Try to drag the titlebar, and check the Window menu."), + ) + } +} + +fn open_test_window( + cx: &mut App, + bounds: Bounds, + label: &str, + is_movable: bool, + appears_transparent: bool, + app_owns_titlebar_drag: bool, +) { + let label = SharedString::from(format!( + "{label}\nis_movable: {is_movable}\n\ + appears_transparent: {appears_transparent}\n\ + app_owns_titlebar_drag: {app_owns_titlebar_drag}" + )); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + is_movable, + app_owns_titlebar_drag, + titlebar: Some(TitlebarOptions { + title: Some(label.clone()), + appears_transparent, + ..Default::default() + }), + ..Default::default() + }, + |window, cx| { + cx.new(|cx| { + let focus_handle = cx.focus_handle(); + focus_handle.focus(window, cx); + ExampleWindow { + label, + focus_handle, + } + }) + }, + ) + .unwrap(); +} + +fn run_example() { + application().run(|cx: &mut App| { + let window_size = size(px(420.), px(280.0)); + let base = Bounds::centered(None, window_size, cx); + + // (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row) + let windows = [ + ("Native titlebar, movable", true, false, false, 0.0, 0.0), + ( + "Native titlebar, NOT movable", + false, + false, + false, + 1.0, + 0.0, + ), + ("Custom titlebar, movable", true, true, false, 0.0, 1.0), + ("Custom titlebar, NOT movable", false, true, false, 1.0, 1.0), + ]; + + for (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row) in windows { + let mut bounds = base; + bounds.origin.x += window_size.width * col; + bounds.origin.y += window_size.height * row; + open_test_window( + cx, + bounds, + label, + is_movable, + appears_transparent, + app_owns_titlebar_drag, + ); + } + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 5765fd80aff0b5..fff4664928f4cc 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1495,14 +1495,24 @@ pub struct WindowOptions { /// The kind of window to create pub kind: WindowKind, - /// Whether the window should be movable by the user. - /// - /// On macOS 27, custom titlebar windows that implement their own drag behavior - /// with [`Window::start_window_move`] should set this to `false`; otherwise - /// AppKit can treat the titlebar region as system-owned and delay clicks - /// while disambiguating titlebar double-clicks. + /// Whether the window can be moved by the user. When `false`, the user cannot drag + /// the window (on macOS this sets `NSWindow.isMovable`, which also disables the + /// Window-menu tiling items); programmatic moves are still allowed. pub is_movable: bool, + /// Whether the application owns dragging of the (custom) titlebar, rather than + /// AppKit. Only has an effect on macOS. + /// + /// Set this to `true` for windows that draw their own titlebar and move the window + /// themselves via [`Window::start_window_move`]. It marks the whole content view as + /// app-owned titlebar content, so AppKit neither drags the window from the titlebar + /// nor delays titlebar clicks while disambiguating double-clicks (a delay first + /// observed on macOS 27). It is independent of `is_movable`, so such windows stay + /// user-movable (via their own drag) and keep the Window-menu tiling items enabled. + /// + /// Leave this `false` for windows that rely on AppKit's native titlebar dragging. + pub app_owns_titlebar_drag: bool, + /// Whether the window should be resizable by the user pub is_resizable: bool, @@ -1558,6 +1568,13 @@ pub struct WindowParams { #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] pub is_movable: bool, + /// Whether the application owns dragging of the (custom) titlebar (macOS only) + #[cfg_attr( + any(target_os = "linux", target_os = "freebsd", target_os = "windows"), + allow(dead_code) + )] + pub app_owns_titlebar_drag: bool, + /// Whether the window should be resizable by the user #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] pub is_resizable: bool, @@ -1639,6 +1656,7 @@ impl Default for WindowOptions { show: true, kind: WindowKind::Normal, is_movable: true, + app_owns_titlebar_drag: false, is_resizable: true, is_minimizable: true, display_id: None, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 198fab268e8cfb..fd560761c899f8 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1292,6 +1292,7 @@ impl Window { show, kind, is_movable, + app_owns_titlebar_drag, is_resizable, is_minimizable, display_id, @@ -1320,6 +1321,7 @@ impl Window { titlebar, kind, is_movable, + app_owns_titlebar_drag, is_resizable, is_minimizable, focus, diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 938633d4749118..e14b2dc0d3fd8c 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -288,6 +288,12 @@ unsafe fn build_classes() { accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, ); + decl.add_method( + sel!(_opaqueRectForWindowMoveWhenInTitlebar), + opaque_rect_for_window_move_when_in_titlebar + as extern "C" fn(&Object, Sel) -> NSRect, + ); + decl.add_method( sel!(characterIndexForPoint:), character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64, @@ -512,6 +518,11 @@ struct MacWindowState { external_files_dragged: bool, // Whether the next left-mouse click is also the focusing click. first_mouse: bool, + // When true, the whole content view is reported as app-owned titlebar content via + // `_opaqueRectForWindowMoveWhenInTitlebar`, so AppKit does not drag the window from + // the titlebar or delay titlebar clicks (a delay first observed on macOS 27). Such + // windows draw their own titlebar and move the window via `start_window_move`. + app_owns_titlebar_drag: bool, fullscreen_restore_bounds: Bounds, move_tab_to_new_window_callback: Option>, merge_all_windows_callback: Option>, @@ -743,6 +754,7 @@ impl MacWindow { titlebar, kind, is_movable, + app_owns_titlebar_drag, is_resizable, is_minimizable, focus, @@ -902,6 +914,7 @@ impl MacWindow { do_command_handled: None, external_files_dragged: false, first_mouse: false, + app_owns_titlebar_drag, fullscreen_restore_bounds: Bounds::default(), move_tab_to_new_window_callback: None, merge_all_windows_callback: None, @@ -2851,6 +2864,25 @@ extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL { YES } +// Reports which region of the view AppKit should treat as app-owned titlebar content +// (rather than a system-owned window-move region). When `app_owns_titlebar_drag` is +// true, we claim the entire view so AppKit neither drags the window from the titlebar +// nor waits to disambiguate double-clicks before delivering titlebar clicks (the macOS +// 27 delay); such windows implement dragging themselves via [`Window::start_window_move`]. +// Otherwise we return an empty rect so AppKit's native titlebar dragging keeps working. +// This is independent of `NSWindow.isMovable`, so the Window-menu tiling items stay +// enabled regardless. +extern "C" fn opaque_rect_for_window_move_when_in_titlebar(this: &Object, _: Sel) -> NSRect { + let zero_rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)); + let window_state = unsafe { get_window_state(this) }; + let app_owns_titlebar_drag = window_state.as_ref().lock().app_owns_titlebar_drag; + if app_owns_titlebar_drag { + unsafe { msg_send![this, bounds] } + } else { + zero_rect + } +} + extern "C" fn character_index_for_point(this: &Object, _: Sel, position: NSPoint) -> u64 { let position = screen_point_to_gpui_point(this, position); with_input_handler(this, |input_handler| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7e07e7f0f3fddc..e768a3e1f311cf 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -377,10 +377,13 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO focus: false, show: false, kind: WindowKind::Normal, - // On macOS, Zed handles window movement itself, so disable AppKit's titlebar dragging. - // On other platforms, `is_movable` gates native window dragging (e.g. Windows' - // `HTCAPTION` hit test), so it must remain `true`. - is_movable: cfg!(not(target_os = "macos")), + is_movable: true, + // Zed draws its own titlebar and moves the window via [`Window::start_window_move`], + // so on macOS AppKit should not own titlebar dragging. This avoids the titlebar + // click delay from AppKit's drag disambiguation (first observed on macOS 27) while + // keeping the window movable and the Window-menu tiling items enabled. No-op on + // other platforms. + app_owns_titlebar_drag: true, display_id: display.map(|display| display.id()), window_background: cx.theme().window_background_appearance(), app_id: Some(app_id.to_owned()),