Skip to main content

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}