Skip to main content

xgpui_shell/
app_titlebar.rs

1use super::ShellOptions;
2use gpui::{prelude::*, *};
3
4pub(super) fn configure_window(options: &mut WindowOptions) {
5    configure_for_platform(
6        options,
7        if cfg!(target_os = "macos") {
8            OperatingSystem::Mac
9        } else if cfg!(target_os = "windows") {
10            OperatingSystem::Windows
11        } else {
12            OperatingSystem::Linux
13        },
14    );
15}
16
17fn configure_for_platform(options: &mut WindowOptions, os: OperatingSystem) {
18    if matches!(os, OperatingSystem::Mac | OperatingSystem::Windows) {
19        let titlebar = options
20            .titlebar
21            .get_or_insert_with(TitlebarOptions::default);
22        titlebar.appears_transparent = true;
23        if os == OperatingSystem::Mac {
24            titlebar.traffic_light_position = Some(point(px(9.), px(12.)));
25            options.app_owns_titlebar_drag = true;
26        }
27    }
28}
29
30/// An explicit drag surface inside an app-composed title bar.
31/// Place title text or empty space here; keep buttons/inputs as siblings so their
32/// events never initiate a window move. Fill available space with a styled child.
33/// Double click honors native macOS behavior or resolved desktop title-bar actions.
34#[derive(IntoElement)]
35pub struct ShellDragRegion {
36    id: ElementId,
37    content: AnyElement,
38    options: ShellOptions,
39}
40impl ShellDragRegion {
41    pub fn new(id: impl Into<ElementId>, content: impl IntoElement) -> Self {
42        Self {
43            id: id.into(),
44            content: content.into_any_element(),
45            options: ShellOptions::default(),
46        }
47    }
48    /// Match the surrounding window's title-bar action overrides, if any.
49    pub fn options(mut self, options: ShellOptions) -> Self {
50        self.options = options;
51        self
52    }
53}
54struct DragState(Option<Point<Pixels>>);
55impl Render for DragState {
56    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
57        div()
58    }
59}
60impl RenderOnce for ShellDragRegion {
61    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
62        let actions = self.options.resolved_preferences(&cx.desktop_preferences());
63        let state = window.use_state(cx, |_, _| DragState(None));
64        let mac = cx.platform_info().os == OperatingSystem::Mac;
65        let native_double_click = mac && self.options.double_click_action.is_none();
66        let windows = cx.platform_info().os == OperatingSystem::Windows;
67        div()
68            .id(self.id)
69            .flex_1()
70            .min_w_0()
71            .h_full()
72            .when(windows, |s| s.window_control_area(WindowControlArea::Drag))
73            .when(!windows, |s| {
74                s.on_mouse_down(
75                    MouseButton::Left,
76                    window.listener_for(
77                        &state,
78                        move |state, event: &MouseDownEvent, window, cx| {
79                            cx.stop_propagation();
80                            if event.click_count == 2 {
81                                state.0 = None;
82                                if native_double_click {
83                                    window.titlebar_double_click();
84                                } else {
85                                    actions
86                                        .double_click_action
87                                        .perform_at(window, event.position);
88                                }
89                            } else {
90                                state.0 = Some(event.position);
91                            }
92                        },
93                    ),
94                )
95                .on_mouse_move(window.listener_for(
96                    &state,
97                    |state, event: &MouseMoveEvent, window, _| {
98                        if !event.dragging() {
99                            state.0 = None;
100                            return;
101                        }
102                        if let Some(origin) = state.0 {
103                            let delta = event.position - origin;
104                            if delta.x.abs() >= px(4.) || delta.y.abs() >= px(4.) {
105                                state.0 = None;
106                                window.start_window_move();
107                            }
108                        }
109                    },
110                ))
111                .on_mouse_up(
112                    MouseButton::Left,
113                    window.listener_for(&state, |state, _, _, _| state.0 = None),
114                )
115                .on_mouse_up_out(
116                    MouseButton::Left,
117                    window.listener_for(&state, |state, _, _, _| state.0 = None),
118                )
119                .on_mouse_down_out(window.listener_for(&state, |state, _, _, _| state.0 = None))
120                .on_mouse_down(MouseButton::Middle, move |event, window, cx| {
121                    cx.stop_propagation();
122                    actions
123                        .middle_click_action
124                        .perform_at(window, event.position);
125                })
126                .on_mouse_down(MouseButton::Right, move |event, window, cx| {
127                    cx.stop_propagation();
128                    actions
129                        .right_click_action
130                        .perform_at(window, event.position);
131                })
132            })
133            .child(self.content)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    #[::core::prelude::v1::test]
141    fn mac_composition_keeps_native_controls_and_title_while_claiming_drag() {
142        let mut options = WindowOptions {
143            titlebar: Some(TitlebarOptions {
144                title: Some("App title".into()),
145                ..Default::default()
146            }),
147            ..Default::default()
148        };
149        configure_for_platform(&mut options, OperatingSystem::Mac);
150        let titlebar = options.titlebar.unwrap();
151        assert_eq!(titlebar.title.as_deref(), Some("App title"));
152        assert!(titlebar.appears_transparent);
153        assert!(titlebar.traffic_light_position.is_some());
154        assert!(options.app_owns_titlebar_drag);
155    }
156    #[::core::prelude::v1::test]
157    fn linux_preserves_decoration_negotiation_and_windows_requests_custom_chrome() {
158        let mut linux = WindowOptions {
159            window_decorations: Some(WindowDecorations::Server),
160            ..Default::default()
161        };
162        configure_for_platform(&mut linux, OperatingSystem::Linux);
163        assert_eq!(linux.window_decorations, Some(WindowDecorations::Server));
164        assert!(!linux.titlebar.unwrap().appears_transparent);
165        let mut windows = WindowOptions::default();
166        configure_for_platform(&mut windows, OperatingSystem::Windows);
167        assert!(windows.titlebar.unwrap().appears_transparent);
168        assert!(!windows.app_owns_titlebar_drag);
169    }
170}