Skip to main content

ui/
window.rs

1//! [`frame`] — the border, corners, shadow and resize edges of a window the
2//! system does not decorate. [`content`] — the card an app's main view sits
3//! on, inside that window.
4//!
5//! `Window::window_decorations` answers which of the two is in force, and it
6//! is not a target: GNOME has never implemented `xdg-decoration`, so a Wayland
7//! window there is told `Client` whatever it asked for, while the same binary
8//! under KDE or on X11 is told `Server` and this draws nothing. macOS and
9//! Windows always answer `Server`.
10//!
11//! The window opens transparent or the band shows as a filled rect —
12//! `WindowBackgroundAppearance::Opaque` has the compositor paint the whole
13//! surface, corners included.
14//!
15//! ```ignore
16//! window::frame(self.content(window, cx), window, cx)
17//! ```
18
19use gpui::{
20    App, Bounds, CursorStyle, Decorations, Div, HitboxBehavior, Hsla, IntoElement, MouseButton,
21    Pixels, Point, ResizeEdge, Size, Tiling, Window, canvas, div, point, prelude::*, px, size,
22};
23
24use theme::Theme;
25
26/// Wrap a window's root in the frame it owes.
27///
28/// Under `Decorations::Server` the child is handed back in a full-size div and
29/// nothing else is painted. Under `Decorations::Client` it takes a border,
30/// [`Theme::surface_radius`] corners and [`theme::frame_shadows`], inside a
31/// [`Theme::CLIENT_INSET`] band that resizes the window.
32///
33/// Must be the window's root element: the resize bands are hit-tested against
34/// `Window::window_bounds`, so an origin of anything but the window's own is a
35/// band in the wrong place.
36///
37/// An edge `tiling` reports flush — against a screen edge, or another window
38/// in a tile — keeps its square corner and gives up its band. A maximized
39/// window counts as flush on every edge: X11 window managers do not always
40/// report tiling for one.
41pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div {
42    let Decorations::Client { tiling } = window.window_decorations() else {
43        return div().size_full().child(child);
44    };
45    let tiling = match window.is_maximized() {
46        true => Tiling::tiled(),
47        false => tiling,
48    };
49
50    let inset = px(Theme::CLIENT_INSET);
51    let radius = px(Theme::surface_radius());
52    let border = Theme::of(cx).border;
53    window.set_client_inset(inset);
54
55    div()
56        .size_full()
57        .child(
58            canvas(
59                move |_, window, _| {
60                    let size = window.window_bounds().get_bounds().size;
61                    bands(size, inset)
62                        .into_iter()
63                        .filter(|(edge, _)| live(*edge, tiling))
64                        .map(|(edge, band)| {
65                            (edge, window.insert_hitbox(band, HitboxBehavior::Normal))
66                        })
67                        .collect::<Vec<_>>()
68                },
69                |_, bands, window, _| {
70                    for (edge, band) in bands {
71                        window.set_cursor_style(cursor(edge), &band);
72                    }
73                },
74            )
75            .absolute()
76            .size_full(),
77        )
78        .when(!tiling.top, |frame| frame.pt(inset))
79        .when(!tiling.bottom, |frame| frame.pb(inset))
80        .when(!tiling.left, |frame| frame.pl(inset))
81        .when(!tiling.right, |frame| frame.pr(inset))
82        .on_mouse_down(MouseButton::Left, move |event, window, _| {
83            let size = window.window_bounds().get_bounds().size;
84            if let Some(edge) =
85                resize_edge(event.position, inset, size).filter(|edge| live(*edge, tiling))
86            {
87                window.start_window_resize(edge);
88            }
89        })
90        .child(
91            div()
92                .size_full()
93                // Without this the child's own background paints over the
94                // corners the border rounds.
95                .overflow_hidden()
96                .border_color(border)
97                .when(!tiling.top, |surface| surface.border_t(px(1.0)))
98                .when(!tiling.bottom, |surface| surface.border_b(px(1.0)))
99                .when(!tiling.left, |surface| surface.border_l(px(1.0)))
100                .when(!tiling.right, |surface| surface.border_r(px(1.0)))
101                .when(!(tiling.top || tiling.left), |surface| {
102                    surface.rounded_tl(radius)
103                })
104                .when(!(tiling.top || tiling.right), |surface| {
105                    surface.rounded_tr(radius)
106                })
107                .when(!(tiling.bottom || tiling.left), |surface| {
108                    surface.rounded_bl(radius)
109                })
110                .when(!(tiling.bottom || tiling.right), |surface| {
111                    surface.rounded_br(radius)
112                })
113                .when(!tiling.is_tiled(), |surface| {
114                    surface.shadow(theme::frame_shadows())
115                })
116                .child(child),
117        )
118}
119
120/// The element an app's main view is drawn in.
121///
122/// Floating, it is a card on the window's chrome, [`Theme::sidebar_bg`],
123/// which its parent paints: [`Theme::panel_radius`] corners, a hairline border
124/// and [`Theme::content_bg`] over the chrome. An opaque card casts
125/// [`theme::content_shadows`]; under [`Theme::vibrancy`] it casts none, since a
126/// shadow shows through a translucent fill. Flat, it is a plain
127/// [`Theme::window_bg`] fill with none of those.
128///
129/// The card has no margin. The caller sizes and places it: it is handed back
130/// unsized, as a flex row or column item would want it, and the space around
131/// it — [`Theme::CONTENT_INSET`] off the window's edge and between two cards —
132/// is the parent's padding and gaps.
133pub fn content(floating: bool, cx: &App) -> Div {
134    let theme = Theme::of(cx);
135    match floating {
136        true => card(theme.content_bg(), theme).when(!theme.vibrancy, |card| {
137            card.shadow(theme::content_shadows())
138        }),
139        false => div().bg(theme.window_bg()),
140    }
141}
142
143/// A floating [`content`] card sitting back, beside a raised one: the same
144/// corners and border, [`Theme::content_resting_bg`] over the chrome, and no
145/// shadow.
146pub fn resting(cx: &App) -> Div {
147    let theme = Theme::of(cx);
148    card(theme.content_resting_bg(), theme)
149}
150
151/// The one colour [`content`] shows, chrome included, for an element inside
152/// it that has to cover what is under it in the same colour.
153pub fn content_fill(floating: bool, theme: &Theme) -> Hsla {
154    match floating {
155        true => theme::over(theme.content_bg(), theme.sidebar_bg()),
156        false => theme.window_bg(),
157    }
158}
159
160/// [`content_fill`] for a [`resting`] card.
161pub fn resting_fill(theme: &Theme) -> Hsla {
162    theme::over(theme.content_resting_bg(), theme.sidebar_bg())
163}
164
165fn card(fill: Hsla, theme: &Theme) -> Div {
166    div()
167        .bg(fill)
168        .rounded(px(Theme::panel_radius()))
169        .border_1()
170        .border_color(theme.border)
171        // Without this the child's own background paints over the corners the
172        // border rounds.
173        .overflow_hidden()
174}
175
176/// Which edge a press at `pos` resizes, or `None` for a press on the content.
177///
178/// `inset` is the band's width, and `size` the whole window's — the band runs
179/// inside both, so the corners are `inset` squares and each edge is what is
180/// left of that side between them.
181pub fn resize_edge(pos: Point<Pixels>, inset: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
182    let (top, bottom) = (pos.y < inset, pos.y > size.height - inset);
183    let (left, right) = (pos.x < inset, pos.x > size.width - inset);
184
185    Some(match (top, bottom, left, right) {
186        (true, _, true, _) => ResizeEdge::TopLeft,
187        (true, _, _, true) => ResizeEdge::TopRight,
188        (_, true, true, _) => ResizeEdge::BottomLeft,
189        (_, true, _, true) => ResizeEdge::BottomRight,
190        (true, ..) => ResizeEdge::Top,
191        (_, true, ..) => ResizeEdge::Bottom,
192        (_, _, true, _) => ResizeEdge::Left,
193        (_, _, _, true) => ResizeEdge::Right,
194        _ => return None,
195    })
196}
197
198/// The eight bands, as the rects [`resize_edge`] classifies to. Disjoint: the
199/// corners take their `inset` square and the edges take what is left, so no
200/// press lands in two and the hitboxes need no order between them.
201fn bands(window: Size<Pixels>, inset: Pixels) -> [(ResizeEdge, Bounds<Pixels>); 8] {
202    let band = |x: Pixels, y: Pixels, width: Pixels, height: Pixels| Bounds {
203        origin: point(x, y),
204        size: size(width, height),
205    };
206    let (far_x, far_y) = (window.width - inset, window.height - inset);
207    let (span_x, span_y) = (window.width - inset - inset, window.height - inset - inset);
208    let zero = px(0.0);
209
210    [
211        (ResizeEdge::TopLeft, band(zero, zero, inset, inset)),
212        (ResizeEdge::TopRight, band(far_x, zero, inset, inset)),
213        (ResizeEdge::BottomLeft, band(zero, far_y, inset, inset)),
214        (ResizeEdge::BottomRight, band(far_x, far_y, inset, inset)),
215        (ResizeEdge::Top, band(inset, zero, span_x, inset)),
216        (ResizeEdge::Bottom, band(inset, far_y, span_x, inset)),
217        (ResizeEdge::Left, band(zero, inset, inset, span_y)),
218        (ResizeEdge::Right, band(far_x, inset, inset, span_y)),
219    ]
220}
221
222/// Whether `edge` resizes: none of the sides it touches is flush.
223pub fn live(edge: ResizeEdge, tiling: Tiling) -> bool {
224    let (top, bottom, left, right) = match edge {
225        ResizeEdge::Top => (true, false, false, false),
226        ResizeEdge::Bottom => (false, true, false, false),
227        ResizeEdge::Left => (false, false, true, false),
228        ResizeEdge::Right => (false, false, false, true),
229        ResizeEdge::TopLeft => (true, false, true, false),
230        ResizeEdge::TopRight => (true, false, false, true),
231        ResizeEdge::BottomLeft => (false, true, true, false),
232        ResizeEdge::BottomRight => (false, true, false, true),
233    };
234    !(top && tiling.top || bottom && tiling.bottom || left && tiling.left || right && tiling.right)
235}
236
237/// The pointer an edge shows.
238fn cursor(edge: ResizeEdge) -> CursorStyle {
239    match edge {
240        ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
241        ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight,
242        ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight,
243        ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft,
244    }
245}