Skip to main content

guise/
windowcontrols.rs

1//! `WindowControls` and `ResizeHandles` — the chrome an app has to draw itself
2//! when the platform will not.
3//!
4//! On macOS and Windows the OS draws the close/minimise/zoom buttons and owns
5//! the resize border. On Linux, a client-side-decorated window draws both or
6//! goes without: no buttons, and edges that cannot be dragged.
7//!
8//! Ported from sinclair, which needed exactly this and nothing more. Both
9//! components render on every platform if you ask them to — you decide, not a
10//! `cfg` inside the library, because a `cfg!(target_os)` buried in a component
11//! is impossible to preview from the other side. [`WindowControls::platform`]
12//! is the convenience for the usual case: draw them only where the OS doesn't.
13
14use gpui::prelude::*;
15use gpui::{div, px, App, IntoElement, MouseButton, SharedString, Window, WindowControlArea};
16
17use crate::devtools::Probed;
18use crate::theme::theme;
19
20/// Minimise / maximise / close, for a window the app decorates itself.
21#[derive(IntoElement)]
22pub struct WindowControls {
23    width: f32,
24    height: f32,
25}
26
27impl Default for WindowControls {
28    fn default() -> Self {
29        WindowControls::new()
30    }
31}
32
33impl WindowControls {
34    pub fn new() -> Self {
35        WindowControls {
36            width: 46.0,
37            height: 28.0,
38        }
39    }
40
41    /// Whether this platform leaves the buttons to the app.
42    ///
43    /// True on Linux, false where the OS draws its own. Use it to decide
44    /// whether to render at all:
45    /// `.children(WindowControls::needed().then(WindowControls::new))`.
46    pub fn needed() -> bool {
47        cfg!(target_os = "linux")
48    }
49
50    /// Width of one button (default 46px).
51    pub fn button_width(mut self, width: f32) -> Self {
52        self.width = width;
53        self
54    }
55
56    /// Height of the strip (default 28px).
57    pub fn height(mut self, height: f32) -> Self {
58        self.height = height;
59        self
60    }
61}
62
63impl RenderOnce for WindowControls {
64    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
65        let t = theme(cx);
66        let fg = t.text().hsla();
67        let dim = fg.opacity(0.6);
68        let hover = fg.opacity(0.12);
69        let (width, height) = (self.width, self.height);
70
71        let button = move |id: &'static str, glyph: &'static str| {
72            div()
73                .id(id)
74                .w(px(width))
75                .h_full()
76                .flex()
77                .items_center()
78                .justify_center()
79                .text_color(dim)
80                .hover(move |st| st.bg(hover).text_color(fg))
81                .child(SharedString::new_static(glyph))
82        };
83
84        div()
85            .flex()
86            .items_center()
87            .flex_none()
88            .h(px(height))
89            .child(
90                button("guise-window-min", "\u{2013}")
91                    .window_control_area(WindowControlArea::Min)
92                    .on_click(|_, window, _| window.minimize_window()),
93            )
94            .child(
95                button("guise-window-max", "\u{25a1}")
96                    .window_control_area(WindowControlArea::Max)
97                    .on_click(|_, window, _| window.zoom_window()),
98            )
99            .child(
100                button("guise-window-close", "\u{2715}")
101                    .window_control_area(WindowControlArea::Close)
102                    .on_click(|_, window, _| window.remove_window()),
103            )
104            .probe("WindowControls")
105    }
106}
107
108/// Invisible edge and corner hit-zones that start a window resize.
109///
110/// Absolutely positioned over the whole window, inert in the middle, so it
111/// never swallows a click meant for the app. Put it last in the root element so
112/// the edges sit above the content.
113#[derive(IntoElement)]
114pub struct ResizeHandles {
115    edge: f32,
116    corner: f32,
117}
118
119impl Default for ResizeHandles {
120    fn default() -> Self {
121        ResizeHandles::new()
122    }
123}
124
125impl ResizeHandles {
126    pub fn new() -> Self {
127        ResizeHandles {
128            edge: 6.0,
129            corner: 12.0,
130        }
131    }
132
133    /// Whether this platform leaves the resize border to the app.
134    pub fn needed() -> bool {
135        cfg!(target_os = "linux")
136    }
137
138    /// Thickness of the edge strips (default 6px).
139    pub fn edge(mut self, edge: f32) -> Self {
140        self.edge = edge;
141        self
142    }
143
144    /// Size of the corner squares (default 12px). Corners are larger because
145    /// they steer two axes at once and are the harder target to hit.
146    pub fn corner(mut self, corner: f32) -> Self {
147        self.corner = corner;
148        self
149    }
150}
151
152impl RenderOnce for ResizeHandles {
153    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
154        use gpui::ResizeEdge;
155
156        let zone = |id: &'static str, edge: ResizeEdge| {
157            div()
158                .id(id)
159                .absolute()
160                .on_mouse_down(MouseButton::Left, move |_, window, _| {
161                    window.start_window_resize(edge);
162                })
163        };
164        let (t, c) = (px(self.edge), px(self.corner));
165
166        div()
167            .absolute()
168            .inset_0()
169            .child(
170                zone("guise-resize-t", ResizeEdge::Top)
171                    .top_0()
172                    .left_0()
173                    .right_0()
174                    .h(t),
175            )
176            .child(
177                zone("guise-resize-b", ResizeEdge::Bottom)
178                    .bottom_0()
179                    .left_0()
180                    .right_0()
181                    .h(t),
182            )
183            .child(
184                zone("guise-resize-l", ResizeEdge::Left)
185                    .top_0()
186                    .bottom_0()
187                    .left_0()
188                    .w(t),
189            )
190            .child(
191                zone("guise-resize-r", ResizeEdge::Right)
192                    .top_0()
193                    .bottom_0()
194                    .right_0()
195                    .w(t),
196            )
197            .child(
198                zone("guise-resize-tl", ResizeEdge::TopLeft)
199                    .top_0()
200                    .left_0()
201                    .w(c)
202                    .h(c),
203            )
204            .child(
205                zone("guise-resize-tr", ResizeEdge::TopRight)
206                    .top_0()
207                    .right_0()
208                    .w(c)
209                    .h(c),
210            )
211            .child(
212                zone("guise-resize-bl", ResizeEdge::BottomLeft)
213                    .bottom_0()
214                    .left_0()
215                    .w(c)
216                    .h(c),
217            )
218            .child(
219                zone("guise-resize-br", ResizeEdge::BottomRight)
220                    .bottom_0()
221                    .right_0()
222                    .w(c)
223                    .h(c),
224            )
225            .probe("ResizeHandles")
226    }
227}
228
229/// Clearance to reserve at the leading edge of a custom titlebar so the macOS
230/// traffic lights are not overlapped.
231///
232/// Zero where the OS draws no inset. sinclair's pane-group tab bar doubles as
233/// its titlebar and reserves exactly this.
234pub const TRAFFIC_LIGHT_INSET: f32 = if cfg!(target_os = "macos") { 88.0 } else { 0.0 };