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.
3//!
4//! `Window::window_decorations` answers which of the two is in force, and it
5//! is not a target: GNOME has never implemented `xdg-decoration`, so a Wayland
6//! window there is told `Client` whatever it asked for, while the same binary
7//! under KDE or on X11 is told `Server` and this draws nothing. macOS and
8//! Windows always answer `Server`.
9//!
10//! The window opens transparent or the band shows as a filled rect —
11//! `WindowBackgroundAppearance::Opaque` has the compositor paint the whole
12//! surface, corners included.
13//!
14//! ```ignore
15//! window::frame(self.content(window, cx), window, cx)
16//! ```
17
18use gpui::{
19    App, Bounds, CursorStyle, Decorations, Div, HitboxBehavior, IntoElement, MouseButton, Pixels,
20    Point, ResizeEdge, Size, Tiling, Window, canvas, div, point, prelude::*, px, size,
21};
22
23use theme::Theme;
24
25/// Wrap a window's root in the frame it owes.
26///
27/// Under `Decorations::Server` the child is handed back in a full-size div and
28/// nothing else is painted. Under `Decorations::Client` it takes a border,
29/// [`Theme::surface_radius`] corners and [`theme::frame_shadows`], inside a
30/// [`Theme::CLIENT_INSET`] band that resizes the window.
31///
32/// Must be the window's root element: the resize bands are hit-tested against
33/// `Window::window_bounds`, so an origin of anything but the window's own is a
34/// band in the wrong place.
35///
36/// An edge `tiling` reports flush — against a screen edge, or another window
37/// in a tile — keeps its square corner and gives up its band. A maximized
38/// window counts as flush on every edge: X11 window managers do not always
39/// report tiling for one.
40pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div {
41    let Decorations::Client { tiling } = window.window_decorations() else {
42        return div().size_full().child(child);
43    };
44    let tiling = match window.is_maximized() {
45        true => Tiling::tiled(),
46        false => tiling,
47    };
48
49    let inset = px(Theme::CLIENT_INSET);
50    let radius = px(Theme::surface_radius());
51    let border = Theme::of(cx).border;
52    window.set_client_inset(inset);
53
54    div()
55        .size_full()
56        .child(
57            canvas(
58                move |_, window, _| {
59                    let size = window.window_bounds().get_bounds().size;
60                    bands(size, inset)
61                        .into_iter()
62                        .filter(|(edge, _)| live(*edge, tiling))
63                        .map(|(edge, band)| {
64                            (edge, window.insert_hitbox(band, HitboxBehavior::Normal))
65                        })
66                        .collect::<Vec<_>>()
67                },
68                |_, bands, window, _| {
69                    for (edge, band) in bands {
70                        window.set_cursor_style(cursor(edge), &band);
71                    }
72                },
73            )
74            .absolute()
75            .size_full(),
76        )
77        .when(!tiling.top, |frame| frame.pt(inset))
78        .when(!tiling.bottom, |frame| frame.pb(inset))
79        .when(!tiling.left, |frame| frame.pl(inset))
80        .when(!tiling.right, |frame| frame.pr(inset))
81        .on_mouse_down(MouseButton::Left, move |event, window, _| {
82            let size = window.window_bounds().get_bounds().size;
83            if let Some(edge) =
84                resize_edge(event.position, inset, size).filter(|edge| live(*edge, tiling))
85            {
86                window.start_window_resize(edge);
87            }
88        })
89        .child(
90            div()
91                .size_full()
92                // Without this the child's own background paints over the
93                // corners the border rounds.
94                .overflow_hidden()
95                .border_color(border)
96                .when(!tiling.top, |surface| surface.border_t(px(1.0)))
97                .when(!tiling.bottom, |surface| surface.border_b(px(1.0)))
98                .when(!tiling.left, |surface| surface.border_l(px(1.0)))
99                .when(!tiling.right, |surface| surface.border_r(px(1.0)))
100                .when(!(tiling.top || tiling.left), |surface| {
101                    surface.rounded_tl(radius)
102                })
103                .when(!(tiling.top || tiling.right), |surface| {
104                    surface.rounded_tr(radius)
105                })
106                .when(!(tiling.bottom || tiling.left), |surface| {
107                    surface.rounded_bl(radius)
108                })
109                .when(!(tiling.bottom || tiling.right), |surface| {
110                    surface.rounded_br(radius)
111                })
112                .when(!tiling.is_tiled(), |surface| {
113                    surface.shadow(theme::frame_shadows())
114                })
115                .child(child),
116        )
117}
118
119/// Which edge a press at `pos` resizes, or `None` for a press on the content.
120///
121/// `inset` is the band's width, and `size` the whole window's — the band runs
122/// inside both, so the corners are `inset` squares and each edge is what is
123/// left of that side between them.
124pub fn resize_edge(pos: Point<Pixels>, inset: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
125    let (top, bottom) = (pos.y < inset, pos.y > size.height - inset);
126    let (left, right) = (pos.x < inset, pos.x > size.width - inset);
127
128    Some(match (top, bottom, left, right) {
129        (true, _, true, _) => ResizeEdge::TopLeft,
130        (true, _, _, true) => ResizeEdge::TopRight,
131        (_, true, true, _) => ResizeEdge::BottomLeft,
132        (_, true, _, true) => ResizeEdge::BottomRight,
133        (true, ..) => ResizeEdge::Top,
134        (_, true, ..) => ResizeEdge::Bottom,
135        (_, _, true, _) => ResizeEdge::Left,
136        (_, _, _, true) => ResizeEdge::Right,
137        _ => return None,
138    })
139}
140
141/// The eight bands, as the rects [`resize_edge`] classifies to. Disjoint: the
142/// corners take their `inset` square and the edges take what is left, so no
143/// press lands in two and the hitboxes need no order between them.
144fn bands(window: Size<Pixels>, inset: Pixels) -> [(ResizeEdge, Bounds<Pixels>); 8] {
145    let band = |x: Pixels, y: Pixels, width: Pixels, height: Pixels| Bounds {
146        origin: point(x, y),
147        size: size(width, height),
148    };
149    let (far_x, far_y) = (window.width - inset, window.height - inset);
150    let (span_x, span_y) = (window.width - inset - inset, window.height - inset - inset);
151    let zero = px(0.0);
152
153    [
154        (ResizeEdge::TopLeft, band(zero, zero, inset, inset)),
155        (ResizeEdge::TopRight, band(far_x, zero, inset, inset)),
156        (ResizeEdge::BottomLeft, band(zero, far_y, inset, inset)),
157        (ResizeEdge::BottomRight, band(far_x, far_y, inset, inset)),
158        (ResizeEdge::Top, band(inset, zero, span_x, inset)),
159        (ResizeEdge::Bottom, band(inset, far_y, span_x, inset)),
160        (ResizeEdge::Left, band(zero, inset, inset, span_y)),
161        (ResizeEdge::Right, band(far_x, inset, inset, span_y)),
162    ]
163}
164
165/// Whether `edge` resizes: none of the sides it touches is flush.
166pub fn live(edge: ResizeEdge, tiling: Tiling) -> bool {
167    let (top, bottom, left, right) = match edge {
168        ResizeEdge::Top => (true, false, false, false),
169        ResizeEdge::Bottom => (false, true, false, false),
170        ResizeEdge::Left => (false, false, true, false),
171        ResizeEdge::Right => (false, false, false, true),
172        ResizeEdge::TopLeft => (true, false, true, false),
173        ResizeEdge::TopRight => (true, false, false, true),
174        ResizeEdge::BottomLeft => (false, true, true, false),
175        ResizeEdge::BottomRight => (false, true, false, true),
176    };
177    !(top && tiling.top || bottom && tiling.bottom || left && tiling.left || right && tiling.right)
178}
179
180/// The pointer an edge shows.
181fn cursor(edge: ResizeEdge) -> CursorStyle {
182    match edge {
183        ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
184        ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight,
185        ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight,
186        ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft,
187    }
188}