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