gpui_component/
window_border.rs

1// From:
2// https://github.com/zed-industries/zed/blob/a8afc63a91f6b75528540dcffe73dc8ce0c92ad8/crates/gpui/examples/window_shadow.rs
3use gpui::{
4    canvas, div, point, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, CursorStyle,
5    Decorations, Edges, HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton,
6    ParentElement, Pixels, Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
7};
8
9use crate::ActiveTheme;
10
11#[cfg(not(target_os = "linux"))]
12const SHADOW_SIZE: Pixels = px(0.0);
13#[cfg(target_os = "linux")]
14const SHADOW_SIZE: Pixels = px(12.0);
15const BORDER_SIZE: Pixels = px(1.0);
16pub(crate) const BORDER_RADIUS: Pixels = px(0.0);
17
18/// Create a new window border.
19pub fn window_border() -> WindowBorder {
20    WindowBorder::new()
21}
22
23/// Window border use to render a custom window border and shadow for Linux.
24#[derive(IntoElement, Default)]
25pub struct WindowBorder {
26    children: Vec<AnyElement>,
27}
28
29impl WindowBorder {
30    pub fn new() -> Self {
31        Self {
32            ..Default::default()
33        }
34    }
35}
36
37/// Get the window paddings.
38pub fn window_paddings(window: &Window) -> Edges<Pixels> {
39    match window.window_decorations() {
40        Decorations::Server => Edges::all(px(0.0)),
41        Decorations::Client { tiling } => {
42            let mut paddings = Edges::all(SHADOW_SIZE);
43            if tiling.top {
44                paddings.top = px(0.0);
45            }
46            if tiling.bottom {
47                paddings.bottom = px(0.0);
48            }
49            if tiling.left {
50                paddings.left = px(0.0);
51            }
52            if tiling.right {
53                paddings.right = px(0.0);
54            }
55            paddings
56        }
57    }
58}
59
60impl ParentElement for WindowBorder {
61    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
62        self.children.extend(elements);
63    }
64}
65
66impl RenderOnce for WindowBorder {
67    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
68        let decorations = window.window_decorations();
69        window.set_client_inset(SHADOW_SIZE);
70
71        div()
72            .id("window-backdrop")
73            .bg(gpui::transparent_black())
74            .map(|div| match decorations {
75                Decorations::Server => div,
76                Decorations::Client { tiling, .. } => div
77                    .bg(gpui::transparent_black())
78                    .child(
79                        canvas(
80                            |_bounds, window, _| {
81                                window.insert_hitbox(
82                                    Bounds::new(
83                                        point(px(0.0), px(0.0)),
84                                        window.window_bounds().get_bounds().size,
85                                    ),
86                                    HitboxBehavior::Normal,
87                                )
88                            },
89                            move |_bounds, hitbox, window, _| {
90                                let mouse = window.mouse_position();
91                                let size = window.window_bounds().get_bounds().size;
92                                let Some(edge) = resize_edge(mouse, SHADOW_SIZE, size) else {
93                                    return;
94                                };
95                                window.set_cursor_style(
96                                    match edge {
97                                        ResizeEdge::Top | ResizeEdge::Bottom => {
98                                            CursorStyle::ResizeUpDown
99                                        }
100                                        ResizeEdge::Left | ResizeEdge::Right => {
101                                            CursorStyle::ResizeLeftRight
102                                        }
103                                        ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
104                                            CursorStyle::ResizeUpLeftDownRight
105                                        }
106                                        ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
107                                            CursorStyle::ResizeUpRightDownLeft
108                                        }
109                                    },
110                                    &hitbox,
111                                );
112                            },
113                        )
114                        .size_full()
115                        .absolute(),
116                    )
117                    .when(!(tiling.top || tiling.right), |div| {
118                        div.rounded_tr(BORDER_RADIUS)
119                    })
120                    .when(!(tiling.top || tiling.left), |div| {
121                        div.rounded_tl(BORDER_RADIUS)
122                    })
123                    .when(!tiling.top, |div| div.pt(SHADOW_SIZE))
124                    .when(!tiling.bottom, |div| div.pb(SHADOW_SIZE))
125                    .when(!tiling.left, |div| div.pl(SHADOW_SIZE))
126                    .when(!tiling.right, |div| div.pr(SHADOW_SIZE))
127                    .on_mouse_down(MouseButton::Left, move |_, window, _| {
128                        let size = window.window_bounds().get_bounds().size;
129                        let pos = window.mouse_position();
130
131                        match resize_edge(pos, SHADOW_SIZE, size) {
132                            Some(edge) => window.start_window_resize(edge),
133                            None => {}
134                        };
135                    }),
136            })
137            .size_full()
138            .child(
139                div()
140                    .map(|div| match decorations {
141                        Decorations::Server => div,
142                        Decorations::Client { tiling } => div
143                            .when(!(tiling.top || tiling.right), |div| {
144                                div.rounded_tr(BORDER_RADIUS)
145                            })
146                            .when(!(tiling.top || tiling.left), |div| {
147                                div.rounded_tl(BORDER_RADIUS)
148                            })
149                            .border_color(cx.theme().window_border)
150                            .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
151                            .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
152                            .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
153                            .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
154                            .when(!tiling.is_tiled(), |div| {
155                                div.shadow(vec![gpui::BoxShadow {
156                                    color: Hsla {
157                                        h: 0.,
158                                        s: 0.,
159                                        l: 0.,
160                                        a: 0.3,
161                                    },
162                                    blur_radius: SHADOW_SIZE / 2.,
163                                    spread_radius: px(0.),
164                                    offset: point(px(0.0), px(0.0)),
165                                }])
166                            }),
167                    })
168                    .on_mouse_move(|_e, _, cx| {
169                        cx.stop_propagation();
170                    })
171                    .bg(gpui::transparent_black())
172                    .size_full()
173                    .children(self.children),
174            )
175    }
176}
177
178fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
179    let edge = if pos.y < shadow_size && pos.x < shadow_size {
180        ResizeEdge::TopLeft
181    } else if pos.y < shadow_size && pos.x > size.width - shadow_size {
182        ResizeEdge::TopRight
183    } else if pos.y < shadow_size {
184        ResizeEdge::Top
185    } else if pos.y > size.height - shadow_size && pos.x < shadow_size {
186        ResizeEdge::BottomLeft
187    } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
188        ResizeEdge::BottomRight
189    } else if pos.y > size.height - shadow_size {
190        ResizeEdge::Bottom
191    } else if pos.x < shadow_size {
192        ResizeEdge::Left
193    } else if pos.x > size.width - shadow_size {
194        ResizeEdge::Right
195    } else {
196        return None;
197    };
198    Some(edge)
199}