ui/titlebar.rs
1//! [`titlebar`] — the strip a window with no system titlebar moves itself by.
2//!
3//! Two platform facts it exists to carry. The window is moved with
4//! `Window::start_window_move` on the first *motion* after a press, never on
5//! the press: a bar that moved on mouse-down would swallow every click on the
6//! buttons sitting in it. And the macOS traffic lights need
7//! [`Theme::TRAFFIC_LIGHT_INSET`] of leading room, which is nothing until the
8//! window goes full screen and AppKit takes them away.
9//!
10//! The window it belongs to opens with `appears_transparent: true` and
11//! **`app_owns_titlebar_drag: true`** — the second one stops AppKit from
12//! dragging the window *and* from delaying titlebar clicks while it waits to
13//! see a double-click.
14//!
15//! ```ignore
16//! titlebar::titlebar("titlebar", &self.drag, true, window)
17//! .px(px(8.0))
18//! .child(/* … */)
19//! ```
20
21use std::{cell::Cell, rc::Rc};
22
23use gpui::{Div, ElementId, MouseButton, Stateful, Window, div, prelude::*, px};
24
25use theme::Theme;
26
27/// Whether the press on a [`titlebar`] is still a candidate for a window move.
28///
29/// Shaped like [`crate::scroll::FollowState`] and for the same reason: it
30/// mutates through `&self`, so the element carries the whole gesture and the
31/// view holds one field.
32#[derive(Clone, Default)]
33pub struct DragState(Rc<Cell<bool>>);
34
35/// The strip: full width, [`Theme::TITLEBAR_HEIGHT`] tall, dragging its window
36/// and zooming it on a double click.
37///
38/// `traffic_lights` reserves the leading inset for the macOS buttons — pass it
39/// on the one strip they sit over, and it stands down in full screen, where
40/// they are gone and the gap would be a hole.
41pub fn titlebar(
42 id: impl Into<ElementId>,
43 drag: &DragState,
44 traffic_lights: bool,
45 window: &Window,
46) -> Stateful<Div> {
47 let (armed, disarm, release) = (drag.0.clone(), drag.0.clone(), drag.0.clone());
48 let moving = drag.0.clone();
49 div()
50 .id(id)
51 .w_full()
52 .h(px(Theme::TITLEBAR_HEIGHT))
53 .flex()
54 .flex_row()
55 .items_center()
56 .when(traffic_lights && !window.is_fullscreen(), |bar| {
57 bar.pl(px(Theme::TRAFFIC_LIGHT_INSET))
58 })
59 .on_mouse_down(MouseButton::Left, move |_, _, _| armed.set(true))
60 .on_mouse_up(MouseButton::Left, move |_, _, _| release.set(false))
61 // A press that leaves the bar is not a window move either — without
62 // this the flag survives, and the next stray motion over the bar drags
63 // the window with no button held.
64 .on_mouse_down_out(move |_, _, _| disarm.set(false))
65 .on_mouse_move(move |_, window, _| {
66 if moving.replace(false) {
67 window.start_window_move();
68 }
69 })
70 // The system's own gesture, whatever the user set it to — zoom,
71 // minimise or nothing. A no-op off macOS.
72 .on_click(|click, window, _| {
73 if click.click_count() == 2 {
74 window.titlebar_double_click();
75 }
76 })
77}