gpui_component/
drawer.rs

1use std::{rc::Rc, time::Duration};
2
3use gpui::{
4    anchored, div, point, prelude::FluentBuilder as _, px, Animation, AnimationExt as _,
5    AnyElement, App, Axis, ClickEvent, DefiniteLength, DismissEvent, Div, EventEmitter,
6    FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement,
7    Pixels, RenderOnce, Styled, Window,
8};
9
10use crate::{
11    actions::Cancel,
12    button::{Button, ButtonVariants as _},
13    h_flex,
14    modal::overlay_color,
15    root::ContextModal as _,
16    title_bar::TITLE_BAR_HEIGHT,
17    v_flex, ActiveTheme, IconName, Placement, Sizable, StyledExt as _,
18};
19
20const CONTEXT: &str = "Drawer";
21pub(crate) fn init(cx: &mut App) {
22    cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
23}
24
25#[derive(IntoElement)]
26pub struct Drawer {
27    pub(crate) focus_handle: FocusHandle,
28    pub(crate) placement: Placement,
29    pub(crate) size: DefiniteLength,
30    resizable: bool,
31    on_close: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
32    title: Option<AnyElement>,
33    footer: Option<AnyElement>,
34    content: Div,
35    margin_top: Pixels,
36    overlay: bool,
37    overlay_closable: bool,
38}
39
40impl Drawer {
41    pub fn new(_: &mut Window, cx: &mut App) -> Self {
42        Self {
43            focus_handle: cx.focus_handle(),
44            placement: Placement::Right,
45            size: DefiniteLength::Absolute(px(350.).into()),
46            resizable: true,
47            title: None,
48            footer: None,
49            content: v_flex().px_4().py_3(),
50            margin_top: TITLE_BAR_HEIGHT,
51            overlay: true,
52            overlay_closable: true,
53            on_close: Rc::new(|_, _, _| {}),
54        }
55    }
56
57    /// Sets the title of the drawer.
58    pub fn title(mut self, title: impl IntoElement) -> Self {
59        self.title = Some(title.into_any_element());
60        self
61    }
62
63    /// Set the footer of the drawer.
64    pub fn footer(mut self, footer: impl IntoElement) -> Self {
65        self.footer = Some(footer.into_any_element());
66        self
67    }
68
69    /// Sets the size of the drawer, default is 350px.
70    pub fn size(mut self, size: impl Into<DefiniteLength>) -> Self {
71        self.size = size.into();
72        self
73    }
74
75    /// Sets the margin top of the drawer, default is 0px.
76    ///
77    /// This is used to let Drawer be placed below a Windows Title, you can give the height of the title bar.
78    pub fn margin_top(mut self, top: Pixels) -> Self {
79        self.margin_top = top;
80        self
81    }
82
83    /// Sets whether the drawer is resizable, default is `true`.
84    pub fn resizable(mut self, resizable: bool) -> Self {
85        self.resizable = resizable;
86        self
87    }
88
89    /// Set whether the drawer should have an overlay, default is `true`.
90    pub fn overlay(mut self, overlay: bool) -> Self {
91        self.overlay = overlay;
92        self
93    }
94
95    /// Set whether the drawer should be closable by clicking the overlay, default is `true`.
96    pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
97        self.overlay_closable = overlay_closable;
98        self
99    }
100
101    /// Listen to the close event of the drawer.
102    pub fn on_close(
103        mut self,
104        on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
105    ) -> Self {
106        self.on_close = Rc::new(on_close);
107        self
108    }
109}
110
111impl EventEmitter<DismissEvent> for Drawer {}
112impl ParentElement for Drawer {
113    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
114        self.content.extend(elements);
115    }
116}
117impl Styled for Drawer {
118    fn style(&mut self) -> &mut gpui::StyleRefinement {
119        self.content.style()
120    }
121}
122
123impl RenderOnce for Drawer {
124    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
125        let placement = self.placement;
126        let titlebar_height = self.margin_top;
127        let window_paddings = crate::window_border::window_paddings(window);
128        let size = window.viewport_size()
129            - gpui::size(
130                window_paddings.left + window_paddings.right,
131                window_paddings.top + window_paddings.bottom,
132            );
133        let on_close = self.on_close.clone();
134
135        anchored()
136            .position(point(
137                window_paddings.left,
138                window_paddings.top + titlebar_height,
139            ))
140            .snap_to_window()
141            .child(
142                div()
143                    .occlude()
144                    .w(size.width)
145                    .h(size.height - titlebar_height)
146                    .bg(overlay_color(self.overlay, cx))
147                    .when(self.overlay_closable, |this| {
148                        this.on_mouse_down(MouseButton::Left, {
149                            let on_close = self.on_close.clone();
150                            move |_, window, cx| {
151                                on_close(&ClickEvent::default(), window, cx);
152                                window.close_drawer(cx);
153                            }
154                        })
155                    })
156                    .child(
157                        v_flex()
158                            .id("drawer")
159                            .tab_group()
160                            .key_context(CONTEXT)
161                            .track_focus(&self.focus_handle)
162                            .on_action({
163                                let on_close = self.on_close.clone();
164                                move |_: &Cancel, window, cx| {
165                                    cx.propagate();
166
167                                    on_close(&ClickEvent::default(), window, cx);
168                                    window.close_drawer(cx);
169                                }
170                            })
171                            .absolute()
172                            .occlude()
173                            .bg(cx.theme().background)
174                            .border_color(cx.theme().border)
175                            .shadow_xl()
176                            .map(|this| {
177                                // Set the size of the drawer.
178                                if placement.is_horizontal() {
179                                    this.h_full().w(self.size)
180                                } else {
181                                    this.w_full().h(self.size)
182                                }
183                            })
184                            .map(|this| match self.placement {
185                                Placement::Top => this.top_0().left_0().right_0().border_b_1(),
186                                Placement::Right => this.top_0().right_0().bottom_0().border_l_1(),
187                                Placement::Bottom => {
188                                    this.bottom_0().left_0().right_0().border_t_1()
189                                }
190                                Placement::Left => this.top_0().left_0().bottom_0().border_r_1(),
191                            })
192                            .child(
193                                // TitleBar
194                                h_flex()
195                                    .justify_between()
196                                    .pl_4()
197                                    .pr_3()
198                                    .py_2()
199                                    .w_full()
200                                    .font_semibold()
201                                    .child(self.title.unwrap_or(div().into_any_element()))
202                                    .child(
203                                        Button::new("close")
204                                            .small()
205                                            .ghost()
206                                            .icon(IconName::Close)
207                                            .on_click(move |_, window, cx| {
208                                                on_close(&ClickEvent::default(), window, cx);
209                                                window.close_drawer(cx);
210                                            }),
211                                    ),
212                            )
213                            .child(
214                                // Body
215                                div()
216                                    .flex_1()
217                                    .overflow_hidden()
218                                    .child(v_flex().scrollable(Axis::Vertical).child(self.content)),
219                            )
220                            .when_some(self.footer, |this, footer| {
221                                // Footer
222                                this.child(
223                                    h_flex()
224                                        .justify_between()
225                                        .px_4()
226                                        .py_3()
227                                        .w_full()
228                                        .child(footer),
229                                )
230                            })
231                            .with_animation(
232                                "slide",
233                                Animation::new(Duration::from_secs_f64(0.15)),
234                                move |this, delta| {
235                                    let y = px(-100.) + delta * px(100.);
236                                    this.map(|this| match placement {
237                                        Placement::Top => this.top(y),
238                                        Placement::Right => this.right(y),
239                                        Placement::Bottom => this.bottom(y),
240                                        Placement::Left => this.left(y),
241                                    })
242                                },
243                            ),
244                    ),
245            )
246    }
247}