Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/gpui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
125 changes: 125 additions & 0 deletions crates/gpui/examples/window_movable.rs
Original file line number Diff line number Diff line change
@@ -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<Self>) -> 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<gpui::Pixels>,
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();
}
30 changes: 24 additions & 6 deletions crates/gpui/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ impl Window {
show,
kind,
is_movable,
app_owns_titlebar_drag,
is_resizable,
is_minimizable,
display_id,
Expand Down Expand Up @@ -1320,6 +1321,7 @@ impl Window {
titlebar,
kind,
is_movable,
app_owns_titlebar_drag,
is_resizable,
is_minimizable,
focus,
Expand Down
32 changes: 32 additions & 0 deletions crates/gpui_macos/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Pixels>,
move_tab_to_new_window_callback: Option<Box<dyn FnMut()>>,
merge_all_windows_callback: Option<Box<dyn FnMut()>>,
Expand Down Expand Up @@ -743,6 +754,7 @@ impl MacWindow {
titlebar,
kind,
is_movable,
app_owns_titlebar_drag,
is_resizable,
is_minimizable,
focus,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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| {
Expand Down
11 changes: 7 additions & 4 deletions crates/zed/src/zed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,13 @@ pub fn build_window_options(display_uuid: Option<Uuid>, 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()),
Expand Down
Loading