Skip to main content

ui/components/
drawer.rs

1use crate::{ModalFooter, ModalHeader, prelude::*};
2use gpui::{AnyElement, ScrollHandle};
3use smallvec::SmallVec;
4
5/// Default width for a [`Drawer`] (Tailwind `w-96` ~= 384px).
6pub const DRAWER_WIDTH: Pixels = px(384.);
7
8/// A side panel ("slide-over") anchored to the right edge of its container.
9///
10/// Structurally a sibling of [`crate::Modal`] (header/body/footer), but
11/// positioned `absolute` + `right-0` and spanning the full height of its
12/// containing element instead of being centered. The caller is responsible
13/// for placing [`Drawer`] inside a `relative()` + `size_full()` overlay layer
14/// (this component does not render its own backdrop/scrim, matching
15/// [`crate::Modal`]'s existing convention).
16#[derive(IntoElement, RegisterComponent)]
17pub struct Drawer {
18    id: ElementId,
19    body_id: ElementId,
20    header: ModalHeader,
21    children: SmallVec<[AnyElement; 2]>,
22    footer: Option<ModalFooter>,
23    width: Pixels,
24    container_scroll_handle: Option<ScrollHandle>,
25    animate: bool,
26}
27
28impl Drawer {
29    pub fn new(id: impl Into<SharedString>) -> Self {
30        let id = id.into();
31        let body_id = ElementId::Name(format!("{}_body", id).into());
32
33        Self {
34            id: ElementId::Name(id),
35            body_id,
36            header: ModalHeader::new(),
37            children: SmallVec::new(),
38            footer: None,
39            width: DRAWER_WIDTH,
40            container_scroll_handle: None,
41            animate: true,
42        }
43    }
44
45    pub fn header(mut self, header: ModalHeader) -> Self {
46        self.header = header;
47        self
48    }
49
50    pub fn footer(mut self, footer: ModalFooter) -> Self {
51        self.footer = Some(footer);
52        self
53    }
54
55    /// Overrides the default `w-96` (384px) panel width.
56    pub fn width(mut self, width: Pixels) -> Self {
57        self.width = width;
58        self
59    }
60
61    pub fn show_dismiss(mut self, show: bool) -> Self {
62        self.header = self.header.show_dismiss_button(show);
63        self
64    }
65
66    pub fn scroll_handle(mut self, handle: ScrollHandle) -> Self {
67        self.container_scroll_handle = Some(handle);
68        self
69    }
70
71    /// Disables the slide-in animation, e.g. for static previews/snapshots.
72    pub fn animate(mut self, animate: bool) -> Self {
73        self.animate = animate;
74        self
75    }
76}
77
78impl ParentElement for Drawer {
79    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
80        self.children.extend(elements)
81    }
82}
83
84impl RenderOnce for Drawer {
85    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
86        let panel = v_flex()
87            .id(self.id.clone())
88            .absolute()
89            .top_0()
90            .right_0()
91            .h_full()
92            .w(self.width)
93            .bg(semantic::elevated_surface(cx))
94            .border_l_1()
95            .border_color(semantic::border(cx))
96            .shadow_level(Shadow::Xl)
97            .overflow_hidden()
98            .child(self.header)
99            .child(
100                v_flex()
101                    .id(self.body_id)
102                    .flex_1()
103                    .w_full()
104                    .p(DynamicSpacing::Base24.rems(cx))
105                    .gap(DynamicSpacing::Base08.rems(cx))
106                    .when_some(self.container_scroll_handle, |this, handle| {
107                        this.overflow_y_scroll().track_scroll(&handle)
108                    })
109                    .children(self.children),
110            )
111            .children(self.footer);
112
113        if self.animate {
114            panel.animate_in_from_right(false).into_any_element()
115        } else {
116            panel.into_any_element()
117        }
118    }
119}
120
121impl Component for Drawer {
122    fn scope() -> ComponentScope {
123        ComponentScope::Overlays
124    }
125
126    fn description() -> Option<&'static str> {
127        Some(
128            "A side panel (slide-over) anchored to the right edge, for detail views or forms that shouldn't fully replace the current context.",
129        )
130    }
131
132    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
133        Some(
134            example_group(vec![single_example(
135                "Basic",
136                div()
137                    .relative()
138                    .w(px(480.))
139                    .h(px(320.))
140                    .overflow_hidden()
141                    .child(
142                        Drawer::new("drawer-preview")
143                            .animate(false)
144                            .header(
145                                ModalHeader::new()
146                                    .headline("Drawer title")
147                                    .show_dismiss_button(true),
148                            )
149                            .child(Label::new("Drawer body content goes here."))
150                            .footer(
151                                ModalFooter::new()
152                                    .end_slot(Button::new("drawer-confirm", "Save").primary()),
153                            ),
154                    )
155                    .into_any_element(),
156            )])
157            .into_any_element(),
158        )
159    }
160}