Skip to main content

xgpui_shell/
lib.rs

1//! Experimental desktop shell with app-composed title bars and optional Linux client frames.
2//! macOS retains native traffic lights; apps supply their own title-bar content and look.
3use gpui::{prelude::*, *};
4mod app_titlebar;
5mod typography;
6pub use app_titlebar::ShellDragRegion;
7pub use typography::ShellTypography;
8
9use frame::*;
10use std::{cell::RefCell, collections::HashMap};
11
12fn activation_key(event: &KeyDownEvent) -> bool {
13    matches!(event.keystroke.key.as_str(), "enter" | "space")
14        && !event.keystroke.modifiers.control
15        && !event.keystroke.modifiers.alt
16        && !event.keystroke.modifiers.platform
17        && !event.is_held
18}
19
20thread_local! {
21    // Platform title reads are macOS-only. Keep the shell chrome's title locally.
22    static SHELL_TITLES: RefCell<HashMap<WindowId, String>> = RefCell::new(HashMap::new());
23}
24pub trait ShellWindowTitle {
25    fn set_shell_title(&mut self, title: &str);
26    fn shell_title(&self) -> String;
27}
28impl ShellWindowTitle for Window {
29    fn set_shell_title(&mut self, title: &str) {
30        self.set_window_title(title);
31        SHELL_TITLES.with(|titles| {
32            titles
33                .borrow_mut()
34                .insert(self.window_handle().window_id(), title.to_owned())
35        });
36        self.refresh();
37    }
38    fn shell_title(&self) -> String {
39        SHELL_TITLES
40            .with(|titles| {
41                titles
42                    .borrow()
43                    .get(&self.window_handle().window_id())
44                    .cloned()
45            })
46            .filter(|title| !title.trim().is_empty())
47            .unwrap_or_else(|| "XGPUI".to_owned())
48    }
49}
50
51/// Open an application with compositor-provided chrome or a shared client-side frame.
52pub trait ShellApp {
53    fn open_shell_window<V: Render + 'static>(
54        &mut self,
55        options: WindowOptions,
56        shell: ShellOptions,
57        build: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
58    ) -> anyhow::Result<WindowHandle<ShellWindow>>;
59    /// Supply an app-owned title-bar view and a separate content view. Native macOS
60    /// traffic lights remain native; client-frame window controls remain shell-owned.
61    /// Compose arbitrary app controls and use ShellDragRegion only for drag surfaces.
62    fn open_shell_window_with_titlebar<H: Render + 'static, V: Render + 'static>(
63        &mut self,
64        options: WindowOptions,
65        shell: ShellOptions,
66        build: impl FnOnce(&mut Window, &mut App) -> (Entity<H>, Entity<V>),
67    ) -> anyhow::Result<WindowHandle<ShellWindow>>;
68}
69impl ShellApp for App {
70    fn open_shell_window<V: Render + 'static>(
71        &mut self,
72        options: WindowOptions,
73        shell: ShellOptions,
74        build: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
75    ) -> anyhow::Result<WindowHandle<ShellWindow>> {
76        open_shell(self, options, shell, |window, cx| {
77            (None, build(window, cx).into())
78        })
79    }
80
81    fn open_shell_window_with_titlebar<H: Render + 'static, V: Render + 'static>(
82        &mut self,
83        mut options: WindowOptions,
84        shell: ShellOptions,
85        build: impl FnOnce(&mut Window, &mut App) -> (Entity<H>, Entity<V>),
86    ) -> anyhow::Result<WindowHandle<ShellWindow>> {
87        app_titlebar::configure_window(&mut options);
88        open_shell(self, options, shell, |window, cx| {
89            let (titlebar, content) = build(window, cx);
90            (Some(titlebar.into()), content.into())
91        })
92    }
93}
94
95fn open_shell(
96    app: &mut App,
97    options: WindowOptions,
98    shell: ShellOptions,
99    build: impl FnOnce(&mut Window, &mut App) -> (Option<AnyView>, AnyView),
100) -> anyhow::Result<WindowHandle<ShellWindow>> {
101    let show_chrome = options.show;
102    let initial_title = options
103        .titlebar
104        .as_ref()
105        .and_then(|titlebar| titlebar.title.clone());
106    app.open_window(options, |window, cx| {
107        if let Some(title) = initial_title {
108            window.set_shell_title(&title);
109        }
110        let (titlebar, content) = build(window, cx);
111        cx.new(|cx| {
112            window
113                .observe_button_layout_changed(|window, _| window.refresh())
114                .detach();
115            window
116                .observe_window_appearance(|window, _| window.refresh())
117                .detach();
118            cx.observe_window_activation(window, |_, window, _| window.refresh())
119                .detach();
120            cx.observe_window_bounds(window, |_, window, _| window.refresh())
121                .detach();
122            let shell_entity = cx.entity().downgrade();
123            let preferences_subscription = cx.observe_desktop_preferences(move |app| {
124                let _ = shell_entity.update(app, |_, cx| cx.notify());
125            });
126            ShellWindow {
127                _preferences_subscription: preferences_subscription,
128                shell,
129                content,
130                titlebar,
131                show_chrome,
132                window_id: window.window_handle().window_id(),
133            }
134        })
135    })
136}
137
138pub struct ShellWindow {
139    _preferences_subscription: Subscription,
140    shell: ShellOptions,
141    content: AnyView,
142    titlebar: Option<AnyView>,
143    show_chrome: bool,
144    window_id: WindowId,
145}
146impl ShellWindow {
147    /// Replace application-supplied overrides and redraw this window.
148    pub fn set_options(&mut self, options: ShellOptions, cx: &mut Context<Self>) {
149        self.shell = options;
150        cx.notify();
151    }
152}
153impl Drop for ShellWindow {
154    fn drop(&mut self) {
155        let _ = SHELL_TITLES.try_with(|titles| titles.borrow_mut().remove(&self.window_id));
156    }
157}
158impl Render for ShellWindow {
159    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
160        let content = self.content.clone();
161        #[cfg(any(test, target_os = "linux", target_os = "freebsd"))]
162        if self.show_chrome
163            && matches!(
164                cx.platform_info().os,
165                OperatingSystem::Linux | OperatingSystem::FreeBsd
166            )
167            && !window.is_fullscreen()
168            && let Decorations::Client { .. } = window.window_decorations()
169        {
170            let state = ClientFrameState::for_window(&self.shell, window, cx);
171            window.set_client_inset(if state.maximized { px(0.) } else { px(5.) });
172            return render_client_frame(
173                state,
174                content.into_any_element(),
175                self.titlebar.clone().map(IntoElement::into_any_element),
176                true,
177            );
178        }
179        // Clear any prior client frame when switching to server chrome/fullscreen.
180        window.set_client_inset(px(0.));
181        if self.show_chrome
182            && let Some(titlebar) = self.titlebar.clone()
183        {
184            let mut state = ClientFrameState::for_window(&self.shell, window, cx);
185            let mac_native = cx.platform_info().os == OperatingSystem::Mac;
186            let custom_windows = cx.platform_info().os == OperatingSystem::Windows;
187            if mac_native || !custom_windows || window.is_fullscreen() {
188                state.button_layout = WindowButtonLayout {
189                    left: [None; 3],
190                    right: [None; 3],
191                };
192            }
193            #[cfg(target_os = "macos")]
194            if mac_native && !window.is_fullscreen() {
195                let height = state
196                    .preferences
197                    .resolved_look(state.appearance, state.active)
198                    .titlebar_height;
199                window.set_traffic_light_position(point(px(9.), px((height - 16.).max(0.) / 2.)));
200            }
201            let header = render_titlebar(
202                &state,
203                Some(titlebar.into_any_element()),
204                true,
205                mac_native && !window.is_fullscreen(),
206            );
207            return div()
208                .size_full()
209                .flex()
210                .flex_col()
211                .child(header)
212                .child(div().flex_1().min_h_0().child(content))
213                .into_any_element();
214        }
215        content.into_any_element()
216    }
217}
218
219struct ClientFrameState {
220    preferences: ResolvedShellPreferences,
221    appearance: WindowAppearance,
222    active: bool,
223    maximized: bool,
224    tiling: Tiling,
225    controls: WindowControls,
226    resizable: bool,
227    button_layout: WindowButtonLayout,
228    title: SharedString,
229    preview_focus: bool,
230    preview_hover: bool,
231}
232
233impl ClientFrameState {
234    fn for_window(options: &ShellOptions, window: &Window, cx: &App) -> Self {
235        let tiling = match window.window_decorations() {
236            Decorations::Client { tiling } => tiling,
237            Decorations::Server => Tiling::default(),
238        };
239        let mut controls = window.window_controls();
240        controls.minimize &= window.is_minimizable();
241        controls.maximize &= window.is_resizable();
242        Self {
243            preferences: options.resolved_preferences(&cx.desktop_preferences()),
244            appearance: window.appearance(),
245            active: window.is_window_active(),
246            maximized: window.is_maximized(),
247            tiling,
248            controls,
249            resizable: window.is_resizable(),
250            button_layout: cx.button_layout().unwrap_or_else(fallback_button_layout),
251            title: window.shell_title().into(),
252            preview_focus: false,
253            preview_hover: false,
254        }
255    }
256}
257
258fn fallback_button_layout() -> WindowButtonLayout {
259    WindowButtonLayout {
260        left: [None; MAX_BUTTONS_PER_SIDE],
261        right: [
262            Some(WindowButton::Minimize),
263            Some(WindowButton::Maximize),
264            Some(WindowButton::Close),
265        ],
266    }
267}
268
269/// A visual-only client-frame preview inside an application's content area.
270///
271/// Uses the production frame renderer and resolver, but attaches no window actions,
272/// drag/resize regions or focusable frame buttons. It never changes the host window's
273/// decorations, insets or state. Simulated state is not detected desktop information.
274#[derive(IntoElement)]
275pub struct ClientFramePreview {
276    /// Displayed preview title.
277    pub title: SharedString,
278    /// Per-window overrides to preview against the current desktop snapshot.
279    pub options: ShellOptions,
280    /// Simulated appearance, independent of native window appearance.
281    pub appearance: WindowAppearance,
282    /// Simulated activation state.
283    pub active: bool,
284    /// Simulated maximization state.
285    pub maximized: bool,
286    /// Hide preview chrome, as a real fullscreen client frame does.
287    pub fullscreen: bool,
288    /// Simulated tiled edges.
289    pub tiling: Tiling,
290    /// Preview button layout; None uses the current platform layout or the shell fallback.
291    pub button_layout: Option<WindowButtonLayout>,
292    /// Visually simulate keyboard focus on the close button without making it actionable.
293    pub show_focus: bool,
294    /// Visually simulate hovering over close, without a native window action.
295    pub show_hover: bool,
296    content: AnyElement,
297}
298
299impl ClientFramePreview {
300    /// Construct an active, floating, light preview. Give its parent a bounded size.
301    pub fn new(
302        title: impl Into<SharedString>,
303        options: ShellOptions,
304        content: impl IntoElement,
305    ) -> Self {
306        Self {
307            title: title.into(),
308            options,
309            appearance: WindowAppearance::Light,
310            active: true,
311            maximized: false,
312            fullscreen: false,
313            tiling: Tiling::default(),
314            button_layout: None,
315            show_focus: false,
316            show_hover: false,
317            content: content.into_any_element(),
318        }
319    }
320}
321
322impl RenderOnce for ClientFramePreview {
323    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
324        if self.fullscreen {
325            return self.content;
326        }
327        render_client_frame(
328            ClientFrameState {
329                preferences: self.options.resolved_preferences(&cx.desktop_preferences()),
330                appearance: self.appearance,
331                active: self.active,
332                maximized: self.maximized,
333                tiling: self.tiling,
334                controls: WindowControls::default(),
335                resizable: true,
336                button_layout: self
337                    .button_layout
338                    .or_else(|| cx.button_layout())
339                    .unwrap_or_else(fallback_button_layout),
340                title: self.title,
341                preview_focus: self.show_focus,
342                preview_hover: self.show_hover,
343            },
344            self.content,
345            None,
346            false,
347        )
348    }
349}
350
351fn render_client_frame(
352    state: ClientFrameState,
353    content: AnyElement,
354    titlebar: Option<AnyElement>,
355    interactive: bool,
356) -> AnyElement {
357    let maximized = state.maximized;
358    let tiling = state.tiling;
359    let palette = state
360        .preferences
361        .resolved_look(state.appearance, state.active)
362        .colors;
363    let inset = if maximized { px(0.) } else { px(5.) };
364    let title = render_titlebar(&state, titlebar, interactive, false);
365    // The inset handles resizing; content events must not initiate window moves.
366    let mut frame = div()
367        .id("shell-frame")
368        .size_full()
369        .relative()
370        .bg(rgb(palette.border));
371    let edges = [
372        (ResizeEdge::Top, !tiling.top, CursorStyle::ResizeUpDown),
373        (
374            ResizeEdge::Bottom,
375            !tiling.bottom,
376            CursorStyle::ResizeUpDown,
377        ),
378        (ResizeEdge::Left, !tiling.left, CursorStyle::ResizeLeftRight),
379        (
380            ResizeEdge::Right,
381            !tiling.right,
382            CursorStyle::ResizeLeftRight,
383        ),
384        (
385            ResizeEdge::TopLeft,
386            !tiling.top && !tiling.left,
387            CursorStyle::ResizeUpLeftDownRight,
388        ),
389        (
390            ResizeEdge::TopRight,
391            !tiling.top && !tiling.right,
392            CursorStyle::ResizeUpRightDownLeft,
393        ),
394        (
395            ResizeEdge::BottomLeft,
396            !tiling.bottom && !tiling.left,
397            CursorStyle::ResizeUpRightDownLeft,
398        ),
399        (
400            ResizeEdge::BottomRight,
401            !tiling.bottom && !tiling.right,
402            CursorStyle::ResizeUpLeftDownRight,
403        ),
404    ];
405    frame = frame
406        .when(!maximized && !tiling.top, |s| s.pt(inset))
407        .when(!maximized && !tiling.bottom, |s| s.pb(inset))
408        .when(!maximized && !tiling.left, |s| s.pl(inset))
409        .when(!maximized && !tiling.right, |s| s.pr(inset))
410        .child(
411            div()
412                .size_full()
413                .flex()
414                .flex_col()
415                .when(!maximized && !tiling.is_tiled(), |s| {
416                    s.rounded(px(10.)).overflow_hidden()
417                })
418                .child(title)
419                .child(
420                    div()
421                        .flex_1()
422                        .min_h_0()
423                        .min_w_0()
424                        .overflow_hidden()
425                        .child(content),
426                ),
427        );
428    if interactive && !maximized && state.resizable {
429        for (index, (edge, enabled, cursor)) in edges.into_iter().enumerate() {
430            if !enabled {
431                continue;
432            }
433            let region = div()
434                .id(("shell-resize", index))
435                .absolute()
436                .cursor(cursor)
437                .on_mouse_down(MouseButton::Left, move |_, window, cx| {
438                    cx.stop_propagation();
439                    window.start_window_resize(edge);
440                });
441            let region = match edge {
442                ResizeEdge::Top => region.top_0().left(inset).right(inset).h(inset),
443                ResizeEdge::Bottom => region.bottom_0().left(inset).right(inset).h(inset),
444                ResizeEdge::Left => region.left_0().top(inset).bottom(inset).w(inset),
445                ResizeEdge::Right => region.right_0().top(inset).bottom(inset).w(inset),
446                ResizeEdge::TopLeft => region.top_0().left_0().size(inset),
447                ResizeEdge::TopRight => region.top_0().right_0().size(inset),
448                ResizeEdge::BottomLeft => region.bottom_0().left_0().size(inset),
449                ResizeEdge::BottomRight => region.bottom_0().right_0().size(inset),
450            };
451            frame = frame.child(region);
452        }
453    }
454    frame.into_any_element()
455}
456
457fn render_titlebar(
458    state: &ClientFrameState,
459    titlebar: Option<AnyElement>,
460    interactive: bool,
461    mac_native: bool,
462) -> AnyElement {
463    let maximized = state.maximized;
464    let preferences = &state.preferences;
465    let look = preferences.resolved_look(state.appearance, state.active);
466    let palette = look.colors;
467    let scale = preferences.text_scale;
468    let button_size = px(look.button_size);
469    let controls = state.controls;
470    let left_buttons = visible_buttons(state.button_layout.left, controls);
471    let right_buttons = visible_buttons(state.button_layout.right, controls);
472    let side_width = px((left_buttons.len().max(right_buttons.len()) as f32 * 33. + 8.) * scale);
473    let custom_header = titlebar.is_some();
474    let buttons = |side: Vec<WindowButton>, right: bool| {
475        div()
476            .flex()
477            .items_center()
478            .flex_none()
479            .min_w(if custom_header {
480                px((side.len() as f32 * 33. + 8.) * scale)
481            } else {
482                side_width
483            })
484            .when(right, |s| s.justify_end())
485            .children(side.into_iter().map(|button| {
486                let label = control_label(button, maximized);
487                control_button(
488                    button.id(),
489                    label,
490                    palette,
491                    look.focus,
492                    button_size,
493                    interactive,
494                    (
495                        !interactive && state.preview_focus && button == WindowButton::Close,
496                        !interactive && state.preview_hover && button == WindowButton::Close,
497                    ),
498                )
499                .when(interactive && cfg!(target_os = "windows"), |element| {
500                    element.window_control_area(match button {
501                        WindowButton::Close => WindowControlArea::Close,
502                        WindowButton::Minimize => WindowControlArea::Min,
503                        WindowButton::Maximize => WindowControlArea::Max,
504                    })
505                })
506                .when(interactive, |element| {
507                    element
508                        .on_click(move |_, window, cx| {
509                            cx.stop_propagation();
510                            activate_control(button, window);
511                        })
512                        .on_key_down(move |event, window, cx| {
513                            if activation_key(event) {
514                                cx.stop_propagation();
515                                activate_control(button, window);
516                            }
517                        })
518                        .on_a11y_action(AccessibleAction::Click, move |_, window, _| {
519                            activate_control(button, window)
520                        })
521                })
522                .child(if button == WindowButton::Close {
523                    // Draw around the geometric center, independent of font baselines.
524                    canvas(
525                        |_, _, _| (),
526                        move |bounds, _, window, _| {
527                            let margin = px(2. * scale);
528                            let start = bounds.origin + point(margin, margin);
529                            let end = bounds.bottom_right() - point(margin, margin);
530                            for (a, b) in
531                                [(start, end), (point(start.x, end.y), point(end.x, start.y))]
532                            {
533                                let mut path = PathBuilder::stroke(px(2. * scale));
534                                path.move_to(a);
535                                path.line_to(b);
536                                if let Ok(path) = path.build() {
537                                    window.paint_path(path, rgb(palette.text));
538                                }
539                            }
540                        },
541                    )
542                    .size(px(20. * scale))
543                    .into_any_element()
544                } else {
545                    div()
546                        .font(gpui::font(".SystemUIFont"))
547                        .text_size(px(14. * scale))
548                        .child(match button {
549                            WindowButton::Minimize => "−",
550                            WindowButton::Maximize if maximized => "â–£",
551                            _ => "â–¡",
552                        })
553                        .into_any_element()
554                })
555            }))
556    };
557    let title = div()
558        .id("shell-titlebar")
559        .debug_selector(|| "shell-titlebar-bounds".into())
560        .flex_none()
561        .h(px(look.titlebar_height))
562        .px(px(4. * scale))
563        .flex()
564        .items_center()
565        .bg(rgb(palette.header))
566        .text_color(rgb(palette.text))
567        .font(look.typography.font.clone())
568        .text_size(px(look.typography.size))
569        .border_b_1()
570        .border_color(rgb(palette.border))
571        .child(buttons(left_buttons, false))
572        .pl(px(if mac_native { 80.0 } else { 0.0 }))
573        .child(
574            div()
575                .id("shell-title-drag")
576                .flex_1()
577                .min_w_0()
578                .overflow_hidden()
579                .h_full()
580                .flex()
581                .items_center()
582                .justify_center()
583                .when(interactive && titlebar.is_none(), |title| {
584                    title
585                        .on_mouse_down(MouseButton::Left, {
586                            let action = preferences.double_click_action;
587                            move |event, window, _| {
588                                if event.click_count == 2 {
589                                    action.perform_at(window, event.position);
590                                } else {
591                                    window.start_window_move();
592                                }
593                            }
594                        })
595                        .on_mouse_down(MouseButton::Right, {
596                            let action = preferences.right_click_action;
597                            move |event, window, _| action.perform_at(window, event.position)
598                        })
599                        .on_mouse_down(MouseButton::Middle, {
600                            let action = preferences.middle_click_action;
601                            move |event, window, _| action.perform_at(window, event.position)
602                        })
603                })
604                .child(titlebar.unwrap_or_else(|| {
605                    div()
606                        .w_full()
607                        .truncate()
608                        .text_center()
609                        .child(state.title.clone())
610                        .into_any_element()
611                })),
612        )
613        .child(buttons(right_buttons, true));
614    title.into_any_element()
615}
616
617/// Optional per-window overrides. `None` follows desktop preferences, then built-in fallbacks.
618///
619/// Unlike the initial experimental API, these fields are optional: wrap explicit values in
620/// `Some(...)`. This preserves explicit `false` and `TitlebarAction::None` overrides.
621/// Explicit colors take precedence over the desktop/default high-contrast palette;
622/// the application is responsible for their contrast and active/inactive variants.
623/// Native appearance and button placement retain the platform's existing APIs.
624#[derive(Default)]
625pub struct ShellOptions {
626    /// Opaque sRGB 0xRRGGBB background for the entire shared titlebar, including
627    /// reserved control space and padding. Wins over `colors.header`.
628    /// None uses the supplied palette header or the desktop/default header.
629    pub titlebar_background: Option<u32>,
630    /// Explicit shared-frame colors, overriding appearance, activation and the
631    /// default high-contrast palette. Does not theme application content, native
632    /// macOS artwork or compositor decorations, or synchronize native appearance.
633    /// Focus uses `accent`, except in high contrast where it uses `colors.text`.
634    pub colors: Option<ShellColors>,
635    /// Override system high contrast.
636    pub high_contrast: Option<bool>,
637    /// Override the sRGB focus accent as 0xRRGGBB.
638    pub accent: Option<u32>,
639    /// Override logical text/target scaling, independent of device DPI.
640    pub text_scale: Option<f32>,
641    /// Override client-frame typography; None parses the desktop description or uses system UI.
642    pub typography: Option<ShellTypography>,
643    /// Resolve motion policy for consumers; the current frame has no animations.
644    pub reduced_motion: Option<bool>,
645    /// Override the desktop double-click action; fallback is maximize/restore.
646    pub double_click_action: Option<TitlebarAction>,
647    /// Override the desktop middle-click action; fallback is no action.
648    pub middle_click_action: Option<TitlebarAction>,
649    /// Override the desktop right-click action; fallback is the window menu.
650    pub right_click_action: Option<TitlebarAction>,
651}
652
653fn shell_scale(scale: f32) -> f32 {
654    if scale.is_finite() {
655        scale.clamp(0.5, 4.)
656    } else {
657        1.
658    }
659}
660
661/// The values actually used by the shared client frame after overrides and fallbacks.
662#[derive(Clone, Debug, PartialEq)]
663pub struct ResolvedShellPreferences {
664    /// Explicit titlebar background, applied after palette resolution.
665    pub titlebar_background: Option<u32>,
666    /// Explicit palette; None follows appearance, activation and contrast.
667    pub colors: Option<ShellColors>,
668    /// Whether the frame uses high-contrast colors.
669    pub high_contrast: bool,
670    /// Encoded sRGB focus accent, 0xRRGGBB.
671    pub accent: u32,
672    /// Logical text/target scale, clamped to 0.5–4 independently of device DPI.
673    pub text_scale: f32,
674    /// Unscaled client-frame typography.
675    pub typography: ShellTypography,
676    /// Resolved policy; no frame animations are currently implemented.
677    pub reduced_motion: bool,
678    /// Effective title-bar double-click action.
679    pub double_click_action: TitlebarAction,
680    /// Effective title-bar middle-click action.
681    pub middle_click_action: TitlebarAction,
682    /// Effective title-bar right-click action.
683    pub right_click_action: TitlebarAction,
684}
685
686impl ShellOptions {
687    /// Resolve the same values used by the client frame. This does not apply them to
688    /// native/server decorations or alter desktop settings. Useful for diagnostics.
689    pub fn resolved_preferences(&self, desktop: &DesktopPreferences) -> ResolvedShellPreferences {
690        let accent = desktop
691            .accent_color
692            .filter(|c| c.iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v)))
693            .map(|[r, g, b]| {
694                ((r * 255.).round() as u32) << 16
695                    | ((g * 255.).round() as u32) << 8
696                    | (b * 255.).round() as u32
697            });
698        ResolvedShellPreferences {
699            titlebar_background: self.titlebar_background,
700            colors: self.colors,
701            high_contrast: self
702                .high_contrast
703                .or(desktop.high_contrast)
704                .unwrap_or(false),
705            accent: self.accent.or(accent).unwrap_or(0x3584e4),
706            text_scale: shell_scale(self.text_scale.or(desktop.text_scale).unwrap_or(1.)),
707            typography: self
708                .typography
709                .clone()
710                .or_else(|| {
711                    desktop
712                        .font
713                        .as_deref()
714                        .and_then(ShellTypography::from_desktop_description)
715                })
716                .unwrap_or_default()
717                .normalized(),
718            reduced_motion: self
719                .reduced_motion
720                .or(desktop.reduced_motion)
721                .unwrap_or(false),
722            double_click_action: self
723                .double_click_action
724                .or_else(|| {
725                    desktop
726                        .titlebar_double_click
727                        .map(TitlebarAction::from_platform)
728                })
729                .unwrap_or(TitlebarAction::ToggleMaximize),
730            middle_click_action: self
731                .middle_click_action
732                .or_else(|| {
733                    desktop
734                        .titlebar_middle_click
735                        .map(TitlebarAction::from_platform)
736                })
737                .unwrap_or(TitlebarAction::None),
738            right_click_action: self
739                .right_click_action
740                .or_else(|| {
741                    desktop
742                        .titlebar_right_click
743                        .map(TitlebarAction::from_platform)
744                })
745                .unwrap_or(TitlebarAction::Menu),
746        }
747    }
748}
749
750/// Client-frame colors after appearance, activation and contrast resolution.
751#[derive(Clone, Copy, Debug, PartialEq, Eq)]
752pub struct ShellColors {
753    pub header: u32,
754    pub text: u32,
755    pub border: u32,
756    pub hover: u32,
757}
758
759/// The exact logical style tokens consumed by the shared frame renderer.
760/// Does not theme application content or native/server decorations.
761#[derive(Clone, Debug, PartialEq)]
762pub struct ResolvedShellLook {
763    pub colors: ShellColors,
764    /// Effective focus-ring color (foreground in high contrast).
765    pub focus: u32,
766    /// Typography with logical text scaling already applied once.
767    pub typography: ShellTypography,
768    pub titlebar_height: f32,
769    pub button_size: f32,
770    /// Motion policy only; current hover/focus changes are instantaneous.
771    pub reduced_motion: bool,
772}
773
774impl ResolvedShellPreferences {
775    /// Resolve the same style tokens used by the native client frame and its preview.
776    pub fn resolved_look(&self, appearance: WindowAppearance, active: bool) -> ResolvedShellLook {
777        let mut colors = self
778            .colors
779            .unwrap_or_else(|| ShellColors::new(appearance, active, self.high_contrast));
780        if let Some(background) = self.titlebar_background {
781            colors.header = background;
782        }
783        let mut typography = self.typography.clone();
784        typography.size *= self.text_scale;
785        ResolvedShellLook {
786            colors,
787            focus: if self.high_contrast {
788                colors.text
789            } else {
790                self.accent
791            },
792            titlebar_height: (40.0 * self.text_scale)
793                .max(typography.size * 1.5 + 12.0 * self.text_scale),
794            button_size: 33.0 * self.text_scale,
795            typography,
796            reduced_motion: self.reduced_motion,
797        }
798    }
799}
800
801/// Supported title-bar interaction, resolved from desktop preferences or an explicit override.
802#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
803pub enum TitlebarAction {
804    #[default]
805    ToggleMaximize,
806    Minimize,
807    None,
808    /// Display the platform window menu when supported.
809    Menu,
810}
811impl TitlebarAction {
812    fn from_platform(action: gpui::TitlebarAction) -> Self {
813        match action {
814            gpui::TitlebarAction::None => Self::None,
815            gpui::TitlebarAction::ToggleMaximize => Self::ToggleMaximize,
816            gpui::TitlebarAction::Minimize => Self::Minimize,
817            gpui::TitlebarAction::Menu => Self::Menu,
818        }
819    }
820
821    fn perform_at(self, window: &mut Window, position: Point<Pixels>) {
822        match self {
823            Self::ToggleMaximize if window.window_controls().maximize && window.is_resizable() => {
824                window.zoom_window()
825            }
826            Self::Minimize if window.window_controls().minimize && window.is_minimizable() => {
827                window.minimize_window()
828            }
829            Self::Menu if window.window_controls().window_menu => window.show_window_menu(position),
830            _ => (),
831        }
832    }
833}
834
835mod frame {
836    use super::*;
837    pub(super) fn visible_buttons(
838        side: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
839        controls: WindowControls,
840    ) -> Vec<WindowButton> {
841        side.into_iter()
842            .flatten()
843            .filter(|button| match button {
844                WindowButton::Minimize => controls.minimize,
845                WindowButton::Maximize => controls.maximize,
846                WindowButton::Close => true,
847            })
848            .collect()
849    }
850    pub(super) fn control_label(button: WindowButton, maximized: bool) -> &'static str {
851        match button {
852            WindowButton::Minimize => "Minimize window",
853            WindowButton::Maximize if maximized => "Restore window",
854            WindowButton::Maximize => "Maximize window",
855            WindowButton::Close => "Close window",
856        }
857    }
858    pub(super) fn activate_control(button: WindowButton, window: &mut Window) {
859        // Capabilities may have changed between rendering and activation.
860        match button {
861            WindowButton::Minimize
862                if window.window_controls().minimize && window.is_minimizable() =>
863            {
864                window.minimize_window()
865            }
866            WindowButton::Maximize
867                if window.window_controls().maximize && window.is_resizable() =>
868            {
869                window.zoom_window()
870            }
871            WindowButton::Close => window.remove_window(),
872            _ => (),
873        }
874    }
875    pub(super) type Palette = ShellColors;
876    impl Palette {
877        pub(super) fn new(appearance: WindowAppearance, active: bool, contrast: bool) -> Self {
878            let dark = matches!(
879                appearance,
880                WindowAppearance::Dark | WindowAppearance::VibrantDark
881            );
882            match (dark, active, contrast) {
883                (true, _, true) => Self {
884                    header: 0x000000,
885                    text: 0xffffff,
886                    border: 0xffffff,
887                    hover: 0x333333,
888                },
889                (false, _, true) => Self {
890                    header: 0xffffff,
891                    text: 0x000000,
892                    border: 0x000000,
893                    hover: 0xdddddd,
894                },
895                (true, true, false) => Self {
896                    header: 0x303030,
897                    text: 0xf6f5f4,
898                    border: 0x141414,
899                    hover: 0x484848,
900                },
901                (true, false, false) => Self {
902                    header: 0x282828,
903                    text: 0xaaaaaa,
904                    border: 0x202020,
905                    hover: 0x404040,
906                },
907                (false, true, false) => Self {
908                    header: 0xebebeb,
909                    text: 0x202020,
910                    border: 0xb8b8b8,
911                    hover: 0xd8d8d8,
912                },
913                (false, false, false) => Self {
914                    header: 0xf5f5f5,
915                    text: 0x707070,
916                    border: 0xd5d5d5,
917                    hover: 0xe3e3e3,
918                },
919            }
920        }
921    }
922    struct ControlTooltip(&'static str);
923    impl Render for ControlTooltip {
924        fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
925            let palette = Palette::new(window.appearance(), true, false);
926            div()
927                .px_2()
928                .py_1()
929                .rounded_md()
930                .bg(rgb(palette.header))
931                .text_color(rgb(palette.text))
932                .child(self.0)
933        }
934    }
935    pub(super) fn control_button(
936        id: &'static str,
937        label: &'static str,
938        palette: Palette,
939        accent: u32,
940        size: Pixels,
941        interactive: bool,
942        (preview_focus, preview_hover): (bool, bool),
943    ) -> Stateful<Div> {
944        div()
945            .id(id)
946            .role(if interactive {
947                Role::Button
948            } else {
949                Role::Image
950            })
951            .aria_label(if interactive {
952                label.to_owned()
953            } else {
954                format!("Preview: {label}")
955            })
956            .when(interactive, |s| s.focusable().tab_stop(true))
957            .size(size)
958            .flex_none()
959            .flex()
960            .items_center()
961            .justify_center()
962            .rounded_full()
963            .border_2()
964            .border_color(transparent_black())
965            .when(preview_focus, |s| s.border_color(rgb(accent)))
966            .when(preview_hover, |s| s.bg(rgb(palette.hover)))
967            .when(interactive, |s| {
968                s.cursor_pointer().hover(move |s| s.bg(rgb(palette.hover)))
969            })
970            .focus_visible(move |s| s.border_color(rgb(accent)))
971            .when(interactive, |s| {
972                s.tooltip(move |_, cx| cx.new(|_| ControlTooltip(label)).into())
973                    .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
974            })
975    }
976
977    #[cfg(test)]
978    mod tests {
979        use super::*;
980        #[::core::prelude::v1::test]
981        fn omitted_and_unavailable_buttons_are_not_reintroduced() {
982            let close_only = WindowButtonLayout::parse(":close").unwrap();
983            assert_eq!(
984                visible_buttons(close_only.right, WindowControls::default()),
985                vec![WindowButton::Close]
986            );
987            let none = WindowButtonLayout::parse(":").unwrap();
988            assert!(visible_buttons(none.left, WindowControls::default()).is_empty());
989            assert!(visible_buttons(none.right, WindowControls::default()).is_empty());
990            let controls = WindowControls {
991                minimize: false,
992                maximize: false,
993                ..Default::default()
994            };
995            assert_eq!(
996                visible_buttons(WindowButtonLayout::linux_default().right, controls),
997                vec![WindowButton::Close]
998            );
999        }
1000        #[::core::prelude::v1::test]
1001        fn simulated_hover_reaches_the_control_background_without_enabling_actions() {
1002            let palette = Palette::new(WindowAppearance::Light, true, false);
1003            let mut normal = control_button(
1004                "normal",
1005                "Close",
1006                palette,
1007                0x3584e4,
1008                px(33.),
1009                false,
1010                (false, false),
1011            );
1012            let mut hovered = control_button(
1013                "hovered",
1014                "Close",
1015                palette,
1016                0x3584e4,
1017                px(33.),
1018                false,
1019                (false, true),
1020            );
1021            assert!(normal.style().background.is_none());
1022            assert!(hovered.style().background.is_some());
1023        }
1024
1025        #[::core::prelude::v1::test]
1026        fn light_dark_inactive_and_contrast_have_distinct_tokens() {
1027            let light = Palette::new(WindowAppearance::Light, true, false);
1028            let dark = Palette::new(WindowAppearance::Dark, true, false);
1029            assert_ne!(light.header, dark.header);
1030            assert_ne!(
1031                light.text,
1032                Palette::new(WindowAppearance::Light, false, false).text
1033            );
1034            assert_eq!(
1035                Palette::new(WindowAppearance::Dark, true, true).text,
1036                0xffffff
1037            );
1038            assert_eq!(
1039                Palette::new(WindowAppearance::Light, true, true).text,
1040                0x000000
1041            );
1042        }
1043        #[::core::prelude::v1::test]
1044        fn invalid_text_scales_cannot_break_layout() {
1045            assert_eq!(shell_scale(f32::NAN), 1.);
1046            assert_eq!(shell_scale(f32::INFINITY), 1.);
1047            assert_eq!(shell_scale(-1.), 0.5);
1048            assert_eq!(shell_scale(1.5), 1.5);
1049            assert_eq!(shell_scale(100.), 4.);
1050        }
1051    }
1052}
1053
1054#[cfg(test)]
1055mod input_tests {
1056    use super::*;
1057
1058    #[::core::prelude::v1::test]
1059    fn desktop_preferences_follow_live_values_and_explicit_overrides_win() {
1060        let desktop = DesktopPreferences {
1061            high_contrast: Some(true),
1062            accent_color: Some([1., 0., 0.]),
1063            text_scale: Some(1.5),
1064            titlebar_double_click: Some(gpui::TitlebarAction::Minimize),
1065            titlebar_middle_click: Some(gpui::TitlebarAction::Menu),
1066            ..Default::default()
1067        };
1068        let inherited = ShellOptions::default().resolved_preferences(&desktop);
1069        assert!(inherited.high_contrast);
1070        assert_eq!(inherited.accent, 0xff0000);
1071        assert_eq!(inherited.text_scale, 1.5);
1072        assert_eq!(inherited.double_click_action, TitlebarAction::Minimize);
1073        assert_eq!(inherited.middle_click_action, TitlebarAction::Menu);
1074        assert_eq!(inherited.right_click_action, TitlebarAction::Menu);
1075        let overridden = ShellOptions {
1076            high_contrast: Some(false),
1077            accent: Some(0),
1078            text_scale: Some(1.),
1079            double_click_action: Some(TitlebarAction::None),
1080            middle_click_action: Some(TitlebarAction::None),
1081            right_click_action: Some(TitlebarAction::None),
1082            ..Default::default()
1083        }
1084        .resolved_preferences(&desktop);
1085        assert!(!overridden.high_contrast);
1086        assert_eq!(overridden.accent, 0);
1087        assert_eq!(overridden.text_scale, 1.);
1088        assert_eq!(overridden.double_click_action, TitlebarAction::None);
1089        assert_eq!(overridden.middle_click_action, TitlebarAction::None);
1090        assert_eq!(overridden.right_click_action, TitlebarAction::None);
1091        let reset = ShellOptions::default().resolved_preferences(&DesktopPreferences::default());
1092        assert!(!reset.high_contrast);
1093        assert_eq!(reset.text_scale, 1.);
1094        assert_eq!(reset.double_click_action, TitlebarAction::ToggleMaximize);
1095    }
1096
1097    #[::core::prelude::v1::test]
1098    fn look_resolution_applies_typography_scale_once_and_preserves_explicit_policy() {
1099        let desktop = DesktopPreferences {
1100            font: Some("Noto Sans 12".into()),
1101            text_scale: Some(1.5),
1102            reduced_motion: Some(true),
1103            ..Default::default()
1104        };
1105        let resolved = ShellOptions::default().resolved_preferences(&desktop);
1106        assert_eq!(resolved.typography.size, 16.0);
1107        let look = resolved.resolved_look(WindowAppearance::Light, true);
1108        assert_eq!(look.typography.size, 24.0);
1109        assert!(look.titlebar_height >= look.typography.size * 1.5);
1110        assert!(look.reduced_motion);
1111        let options = ShellOptions {
1112            typography: Some(ShellTypography {
1113                font: font("Explicit Family"),
1114                size: 24.0,
1115            }),
1116            reduced_motion: Some(false),
1117            high_contrast: Some(true),
1118            accent: Some(0xff0000),
1119            ..Default::default()
1120        };
1121        let look = options
1122            .resolved_preferences(&desktop)
1123            .resolved_look(WindowAppearance::Dark, false);
1124        assert_eq!(look.typography.font.family.as_ref(), "Explicit Family");
1125        assert_eq!(look.typography.size, 36.0);
1126        assert_eq!(look.focus, look.colors.text);
1127        assert!(!look.reduced_motion);
1128        let reset = ShellOptions::default().resolved_preferences(&DesktopPreferences {
1129            font: Some("Sans Condensed 12".into()),
1130            ..Default::default()
1131        });
1132        assert_eq!(reset.typography, ShellTypography::default());
1133    }
1134
1135    #[::core::prelude::v1::test]
1136    fn color_overrides_and_resets_follow_explicit_precedence() {
1137        let desktop = DesktopPreferences {
1138            high_contrast: Some(true),
1139            ..Default::default()
1140        };
1141        let palette = ShellColors {
1142            header: 0x123456,
1143            text: 0xffffff,
1144            border: 0x654321,
1145            hover: 0xabcdef,
1146        };
1147        let mut options = ShellOptions {
1148            colors: Some(palette),
1149            titlebar_background: Some(0),
1150            accent: Some(0xff0000),
1151            ..Default::default()
1152        };
1153        for appearance in [WindowAppearance::Light, WindowAppearance::Dark] {
1154            for active in [false, true] {
1155                let look = options
1156                    .resolved_preferences(&desktop)
1157                    .resolved_look(appearance, active);
1158                assert_eq!(
1159                    look.colors,
1160                    ShellColors {
1161                        header: 0,
1162                        ..palette
1163                    }
1164                );
1165                assert_eq!(look.focus, palette.text);
1166            }
1167        }
1168        options.titlebar_background = None;
1169        assert_eq!(
1170            options
1171                .resolved_preferences(&desktop)
1172                .resolved_look(WindowAppearance::Light, true)
1173                .colors,
1174            palette
1175        );
1176        options.high_contrast = Some(false);
1177        assert_eq!(
1178            options
1179                .resolved_preferences(&desktop)
1180                .resolved_look(WindowAppearance::Light, true)
1181                .focus,
1182            0xff0000
1183        );
1184        options.colors = None;
1185        options.titlebar_background = Some(0);
1186        let look = options
1187            .resolved_preferences(&desktop)
1188            .resolved_look(WindowAppearance::Light, true);
1189        assert_eq!(
1190            look.colors,
1191            ShellColors {
1192                header: 0,
1193                ..ShellColors::new(WindowAppearance::Light, true, false)
1194            }
1195        );
1196        options.titlebar_background = None;
1197        options.high_contrast = None;
1198        for active in [false, true] {
1199            assert_eq!(
1200                options
1201                    .resolved_preferences(&desktop)
1202                    .resolved_look(WindowAppearance::Dark, active)
1203                    .colors,
1204                ShellColors::new(WindowAppearance::Dark, active, true)
1205            );
1206        }
1207    }
1208
1209    #[::core::prelude::v1::test]
1210    fn activation_keys_do_not_consume_modified_shortcuts_or_repeats() {
1211        let key = |text: &'static str, held| KeyDownEvent {
1212            keystroke: Keystroke::parse(text).unwrap(),
1213            is_held: held,
1214            prefer_character_input: false,
1215        };
1216        assert!(activation_key(&key("enter", false)));
1217        assert!(activation_key(&key("space", false)));
1218        assert!(!activation_key(&key("ctrl-enter", false)));
1219        assert!(!activation_key(&key("alt-space", false)));
1220        assert!(!activation_key(&key("enter", true)));
1221        assert!(!activation_key(&key("a", false)));
1222    }
1223}
1224
1225#[cfg(test)]
1226mod lifecycle_tests {
1227    use super::*;
1228    use ::core::prelude::v1::test;
1229    struct Content;
1230    impl Render for Content {
1231        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1232            div()
1233        }
1234    }
1235    #[gpui::test]
1236    fn independent_titles_are_released_on_close(cx: &mut TestAppContext) {
1237        let (one, two) = cx.update(|cx| {
1238            let mut open = |title: &str| {
1239                cx.open_shell_window(
1240                    WindowOptions {
1241                        titlebar: Some(TitlebarOptions {
1242                            title: Some(title.to_owned().into()),
1243                            ..Default::default()
1244                        }),
1245                        ..Default::default()
1246                    },
1247                    ShellOptions::default(),
1248                    |_, cx| cx.new(|_| Content),
1249                )
1250                .unwrap()
1251            };
1252            (open("First"), open("Second"))
1253        });
1254        let one_id = one.window_id();
1255        let two_id = two.window_id();
1256        cx.update_window(*one, |_, window, _| {
1257            assert_eq!(window.shell_title(), "First");
1258            window.set_shell_title("Updated");
1259        })
1260        .unwrap();
1261        cx.update_window(*two, |_, window, _| {
1262            assert_eq!(window.shell_title(), "Second")
1263        })
1264        .unwrap();
1265        cx.update_window(*one, |_, window, _| {
1266            assert_eq!(window.shell_title(), "Updated");
1267            window.remove_window();
1268        })
1269        .unwrap();
1270        cx.run_until_parked();
1271        SHELL_TITLES.with(|titles| {
1272            assert!(!titles.borrow().contains_key(&one_id));
1273            assert!(titles.borrow().contains_key(&two_id));
1274        });
1275        cx.update_window(*two, |_, window, _| window.remove_window())
1276            .unwrap();
1277        cx.run_until_parked();
1278        SHELL_TITLES.with(|titles| assert!(!titles.borrow().contains_key(&two_id)));
1279    }
1280}
1281
1282#[cfg(test)]
1283mod preview_tests {
1284    use super::*;
1285    use ::core::prelude::v1::test;
1286
1287    struct PreviewHost;
1288    impl Render for PreviewHost {
1289        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1290            div().size_full().child(ClientFramePreview::new(
1291                "Preview only",
1292                ShellOptions::default(),
1293                div().size_full().child("Content"),
1294            ))
1295        }
1296    }
1297
1298    struct ContentStyleHost(std::rc::Rc<RefCell<Option<SharedString>>>);
1299    impl Render for ContentStyleHost {
1300        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1301            let observed = self.0.clone();
1302            div()
1303                .size_full()
1304                .font_family("Content Typeface")
1305                .child(ClientFramePreview::new(
1306                    "Different chrome typography",
1307                    ShellOptions {
1308                        typography: Some(ShellTypography {
1309                            font: font("Frame Typeface"),
1310                            size: 30.0,
1311                        }),
1312                        ..Default::default()
1313                    },
1314                    canvas(
1315                        move |_, window, _| {
1316                            *observed.borrow_mut() = Some(window.text_style().font_family);
1317                        },
1318                        |_, _, _, _| (),
1319                    )
1320                    .size_full(),
1321                ))
1322        }
1323    }
1324
1325    #[gpui::test]
1326    fn client_frame_typography_does_not_leak_into_application_content(cx: &mut TestAppContext) {
1327        let observed = std::rc::Rc::new(RefCell::new(None));
1328        let copy = observed.clone();
1329        let host = cx.open_window(size(px(600.), px(240.)), move |_, _| ContentStyleHost(copy));
1330        let mut test_window = VisualTestContext::from_window(host.into(), cx);
1331        test_window.update(|window, app| window.draw(app).clear(app));
1332        assert_eq!(
1333            observed.borrow().as_ref().map(|s| s.as_ref()),
1334            Some("Content Typeface")
1335        );
1336    }
1337
1338    #[gpui::test]
1339    fn preview_pointer_events_do_not_change_or_close_the_host(cx: &mut TestAppContext) {
1340        let host = cx.open_window(size(px(600.), px(200.)), |_, _| PreviewHost);
1341        let mut test_window = VisualTestContext::from_window(host.into(), cx);
1342        test_window.update(|window, app| window.draw(app).clear(app));
1343        let before = cx
1344            .update_window(*host, |_, window, _| {
1345                (
1346                    window.window_bounds(),
1347                    window.window_decorations(),
1348                    window.client_inset(),
1349                    window.is_maximized(),
1350                    window.is_fullscreen(),
1351                )
1352            })
1353            .unwrap();
1354        // The three preview window controls, title drag area, and resize edge must
1355        // all remain decorative even though they share the real frame's renderer.
1356        for position in [
1357            point(px(575.), px(20.)),
1358            point(px(542.), px(20.)),
1359            point(px(509.), px(20.)),
1360            point(px(300.), px(20.)),
1361            point(px(2.), px(80.)),
1362        ] {
1363            test_window.simulate_click(position, Modifiers::default());
1364        }
1365        test_window.simulate_event(MouseDownEvent {
1366            position: point(px(300.), px(20.)),
1367            button: MouseButton::Left,
1368            click_count: 2,
1369            first_mouse: false,
1370            modifiers: Modifiers::default(),
1371        });
1372        test_window.simulate_mouse_up(
1373            point(px(300.), px(20.)),
1374            MouseButton::Left,
1375            Modifiers::default(),
1376        );
1377        let after = cx
1378            .update_window(*host, |_, window, _| {
1379                (
1380                    window.window_bounds(),
1381                    window.window_decorations(),
1382                    window.client_inset(),
1383                    window.is_maximized(),
1384                    window.is_fullscreen(),
1385                )
1386            })
1387            .expect("preview close must not close the real window");
1388        assert_eq!(before, after);
1389    }
1390}
1391
1392#[cfg(test)]
1393mod composition_tests {
1394    use super::*;
1395    use ::core::prelude::v1::test;
1396    use std::{cell::Cell, rc::Rc};
1397    struct Header(Rc<Cell<usize>>);
1398    impl Render for Header {
1399        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1400            let clicks = self.0.clone();
1401            div()
1402                .size_full()
1403                .flex()
1404                .items_center()
1405                .child(
1406                    div()
1407                        .id("app-button")
1408                        .debug_selector(|| "app-button".into())
1409                        .w(px(32.))
1410                        .h_full()
1411                        .on_click(move |_, _, _| clicks.set(clicks.get() + 1))
1412                        .child("App"),
1413                )
1414                .child(ShellDragRegion::new(
1415                    "app-drag",
1416                    div().size_full().child("App title"),
1417                ))
1418        }
1419    }
1420    struct Body;
1421    impl Render for Body {
1422        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1423            div()
1424                .debug_selector(|| "app-body".into())
1425                .size_full()
1426                .child("Body")
1427        }
1428    }
1429    #[gpui::test]
1430    fn composed_mac_header_reserves_native_control_space_and_preserves_app_clicks(
1431        cx: &mut TestAppContext,
1432    ) {
1433        cx.set_platform_info(PlatformInfo {
1434            os: OperatingSystem::Mac,
1435            target: PlatformTarget::Native,
1436        });
1437        let clicks = Rc::new(Cell::new(0));
1438        let copy = clicks.clone();
1439        let handle = cx.update(|app| {
1440            app.open_shell_window_with_titlebar(
1441                WindowOptions {
1442                    window_bounds: Some(WindowBounds::Windowed(Bounds {
1443                        origin: Point::default(),
1444                        size: size(px(600.), px(240.)),
1445                    })),
1446                    ..Default::default()
1447                },
1448                ShellOptions::default(),
1449                |_, app| (app.new(|_| Header(copy)), app.new(|_| Body)),
1450            )
1451            .unwrap()
1452        });
1453        let mut visual = VisualTestContext::from_window(handle.into(), cx);
1454        visual.update(|window, app| window.draw(app).clear(app));
1455        let button = visual
1456            .debug_bounds("app-button")
1457            .expect("app header rendered");
1458        let body = visual.debug_bounds("app-body").expect("app body rendered");
1459        let header = visual.debug_bounds("shell-titlebar-bounds").unwrap();
1460        assert!(
1461            button.left() >= px(80.),
1462            "reserve native traffic-light space"
1463        );
1464        assert_eq!(body.top(), header.bottom());
1465        visual.simulate_click(button.center(), Modifiers::default());
1466        assert_eq!(clicks.get(), 1);
1467        assert_eq!(visual.debug_bounds("app-body").unwrap(), body);
1468        let identities = cx.update(|app| {
1469            let shell = handle.read(app).unwrap();
1470            (
1471                shell.content.entity_id(),
1472                shell.titlebar.as_ref().unwrap().entity_id(),
1473            )
1474        });
1475        let focus = visual.update(|window, app| {
1476            let focus = app.focus_handle();
1477            window.focus(&focus, app);
1478            focus
1479        });
1480        for background in [Some(0), Some(0xececed), Some(0xf0f0f0), None] {
1481            cx.update(|app| {
1482                handle
1483                    .update(app, |shell, _, cx| {
1484                        shell.set_options(
1485                            ShellOptions {
1486                                titlebar_background: background,
1487                                ..Default::default()
1488                            },
1489                            cx,
1490                        );
1491                        assert_eq!(
1492                            (
1493                                shell.content.entity_id(),
1494                                shell.titlebar.as_ref().unwrap().entity_id()
1495                            ),
1496                            identities
1497                        );
1498                    })
1499                    .unwrap();
1500            });
1501            visual.update(|window, app| window.draw(app).clear(app));
1502            assert_eq!(visual.debug_bounds("app-body").unwrap(), body);
1503            visual.update(|window, _| assert!(focus.is_focused(window)));
1504            if let Some(background) = background {
1505                visual.update(|window, _| {
1506                    let quad = window
1507                        .painted_quads()
1508                        .into_iter()
1509                        .find(|quad| quad.background == rgb(background).into())
1510                        .expect("shared header background is painted");
1511                    let visible = quad.bounds.intersect(&quad.content_mask.bounds);
1512                    assert_eq!(visible, header.scale(window.scale_factor()));
1513                });
1514            }
1515            visual.simulate_click(button.center(), Modifiers::default());
1516        }
1517        assert_eq!(
1518            clicks.get(),
1519            5,
1520            "one callback per click through option updates"
1521        );
1522    }
1523}