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(
73        mut self,
74        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
75    ) -> Self {
76        self.on_close = Some(Rc::new(handler));
77        self
78    }
79}
80
81impl Default for Drawer {
82    fn default() -> Self {
83        Drawer::new()
84    }
85}
86
87impl ParentElement for Drawer {
88    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
89        self.children.extend(elements);
90    }
91}
92
93impl RenderOnce for Drawer {
94    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
95        let t = theme(cx);
96        let surface = t.surface().hsla();
97        let text = t.text().hsla();
98        let dimmed = t.dimmed().hsla();
99        let scrim = t.black.alpha(0.55);
100        let padding = t.spacing(self.padding);
101        let gap = t.spacing(Size::Md);
102        let viewport = window.viewport_size();
103        let close = self.on_close.clone();
104
105        let mut panel = div()
106            .id("guise-drawer-panel")
107            .occlude()
108            .flex()
109            .flex_col()
110            .gap(px(gap))
111            .bg(surface)
112            .p(px(padding))
113            .shadow_xl()
114            .on_click(|_ev, _window, cx| cx.stop_propagation());
115        panel = match self.side {
116            Side::Left | Side::Right => panel.h(viewport.height).w(px(self.size)),
117            Side::Top | Side::Bottom => panel.w(viewport.width).h(px(self.size)),
118        };
119
120        if self.title.is_some() || close.is_some() {
121            let mut header = div().flex().items_center().justify_between();
122            header = header.child(match self.title {
123                Some(title) => div()
124                    .text_size(px(18.0))
125                    .font_weight(FontWeight::BOLD)
126                    .text_color(text)
127                    .child(title),
128                None => div(),
129            });
130            if let Some(handler) = close.clone() {
131                header = header.child(
132                    div()
133                        .id("guise-drawer-close")
134                        .px(px(6.0))
135                        .rounded(px(4.0))
136                        .text_size(px(18.0))
137                        .text_color(dimmed)
138                        .hover(move |s| s.text_color(text))
139                        .child(SharedString::new_static("\u{00d7}"))
140                        .on_click(move |ev, window, cx| {
141                            handler(ev, window, cx);
142                            cx.stop_propagation();
143                        }),
144                );
145            }
146            panel = panel.child(header);
147        }
148        panel = panel.children(self.children);
149
150        let mut backdrop = div()
151            .id("guise-drawer-backdrop")
152            .occlude()
153            .absolute()
154            .top(px(0.0))
155            .left(px(0.0))
156            .w(viewport.width)
157            .h(viewport.height)
158            .flex()
159            .bg(scrim);
160        backdrop = match self.side {
161            Side::Left => backdrop.flex_row().justify_start(),
162            Side::Right => backdrop.flex_row().justify_end(),
163            Side::Top => backdrop.flex_col().justify_start(),
164            Side::Bottom => backdrop.flex_col().justify_end(),
165        };
166        backdrop = backdrop.child(panel);
167
168        if let Some(handler) = close {
169            backdrop = backdrop.on_click(move |ev, window, cx| handler(ev, window, cx));
170        }
171
172        deferred(backdrop)
173    }
174}