Skip to main content

guise/overlay/
drawer.rs

1//! `Drawer` — a panel that slides in from a window edge over a scrim.
2//!
3//! Controlled like [`Modal`](super::Modal): the parent owns `opened` and renders
4//! the `Drawer` only while true, passing an `on_close` handler. Place it as a
5//! child of a full-size root. Clicking the scrim or close button invokes
6//! `on_close`.
7
8use std::rc::Rc;
9
10use gpui::prelude::*;
11use gpui::{
12    deferred, div, px, AnyElement, App, ClickEvent, FontWeight, IntoElement, SharedString, Window,
13};
14
15use crate::devtools::ProbedAny;
16use crate::theme::{theme, Size};
17
18type CloseHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
19
20/// Which edge the drawer slides from.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Side {
23    Left,
24    Right,
25    Top,
26    Bottom,
27}
28
29/// A slide-in panel.
30#[derive(IntoElement)]
31pub struct Drawer {
32    title: Option<SharedString>,
33    children: Vec<AnyElement>,
34    side: Side,
35    size: f32,
36    padding: Size,
37    on_close: Option<CloseHandler>,
38}
39
40impl Drawer {
41    pub fn new() -> Self {
42        Drawer {
43            title: None,
44            children: Vec::new(),
45            side: Side::Right,
46            size: 360.0,
47            padding: Size::Lg,
48            on_close: None,
49        }
50    }
51
52    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
53        self.title = Some(title.into());
54        self
55    }
56
57    pub fn side(mut self, side: Side) -> Self {
58        self.side = side;
59        self
60    }
61
62    /// Width for left/right drawers, height for top/bottom (px).
63    pub fn size(mut self, size: f32) -> Self {
64        self.size = size;
65        self
66    }
67
68    pub fn padding(mut self, padding: Size) -> Self {
69        self.padding = padding;
70        self
71    }
72
73    pub fn on_close(
74        mut self,
75        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
76    ) -> Self {
77        self.on_close = Some(Rc::new(handler));
78        self
79    }
80}
81
82impl Default for Drawer {
83    fn default() -> Self {
84        Drawer::new()
85    }
86}
87
88impl ParentElement for Drawer {
89    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
90        self.children.extend(elements);
91    }
92}
93
94impl RenderOnce for Drawer {
95    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
96        let t = theme(cx);
97        let surface = t.surface().hsla();
98        let text = t.text().hsla();
99        let dimmed = t.dimmed().hsla();
100        let scrim = t.black.alpha(0.55);
101        let padding = t.spacing(self.padding);
102        let gap = t.spacing(Size::Md);
103        let viewport = window.viewport_size();
104        let close = self.on_close.clone();
105
106        let mut panel = div()
107            .id("guise-drawer-panel")
108            .occlude()
109            .flex()
110            .flex_col()
111            .gap(px(gap))
112            .bg(surface)
113            .p(px(padding))
114            .shadow_xl()
115            .on_click(|_ev, _window, cx| cx.stop_propagation());
116        panel = match self.side {
117            Side::Left | Side::Right => panel.h(viewport.height).w(px(self.size)),
118            Side::Top | Side::Bottom => panel.w(viewport.width).h(px(self.size)),
119        };
120
121        if self.title.is_some() || close.is_some() {
122            let mut header = div().flex().items_center().justify_between();
123            header = header.child(match self.title {
124                Some(title) => div()
125                    .text_size(px(18.0))
126                    .font_weight(FontWeight::BOLD)
127                    .text_color(text)
128                    .child(title),
129                None => div(),
130            });
131            if let Some(handler) = close.clone() {
132                header = header.child(
133                    div()
134                        .id("guise-drawer-close")
135                        .px(px(6.0))
136                        .rounded(px(4.0))
137                        .text_size(px(18.0))
138                        .text_color(dimmed)
139                        .hover(move |s| s.text_color(text))
140                        .child(SharedString::new_static("\u{00d7}"))
141                        .on_click(move |ev, window, cx| {
142                            handler(ev, window, cx);
143                            cx.stop_propagation();
144                        }),
145                );
146            }
147            panel = panel.child(header);
148        }
149        panel = panel.children(self.children);
150
151        let mut backdrop = div()
152            .id("guise-drawer-backdrop")
153            .occlude()
154            .absolute()
155            .top(px(0.0))
156            .left(px(0.0))
157            .w(viewport.width)
158            .h(viewport.height)
159            .flex()
160            .bg(scrim);
161        backdrop = match self.side {
162            Side::Left => backdrop.flex_row().justify_start(),
163            Side::Right => backdrop.flex_row().justify_end(),
164            Side::Top => backdrop.flex_col().justify_start(),
165            Side::Bottom => backdrop.flex_col().justify_end(),
166        };
167        backdrop = backdrop.child(panel);
168
169        if let Some(handler) = close {
170            backdrop = backdrop.on_click(move |ev, window, cx| handler(ev, window, cx));
171        }
172
173        deferred(backdrop).probe_any("Drawer")
174    }
175}