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 };