Skip to main content

rgpui_component/
sheet.rs

1use std::{rc::Rc, time::Duration};
2
3use rgpui::{
4    Animation, AnimationExt as _, AnyElement, App, ClickEvent, DefiniteLength, DismissEvent, Edges,
5    EventEmitter, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
6    ParentElement, Pixels, RenderOnce, StyleRefinement, Styled, Window, WindowControlArea,
7    anchored, div, point, prelude::FluentBuilder as _, px,
8};
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    ActiveTheme, FocusTrapElement as _, IconName, Placement, Sizable, StyledExt as _,
14    WindowExt as _,
15    actions::Cancel,
16    button::{Button, ButtonVariants as _},
17    dialog::overlay_color,
18    h_flex,
19    scroll::ScrollableElement as _,
20    title_bar::TITLE_BAR_HEIGHT,
21    v_flex,
22};
23
24const CONTEXT: &str = "Sheet";
25pub(crate) fn init(cx: &mut App) {
26    cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
27}
28
29/// The settings for sheets.
30#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
31pub struct SheetSettings {
32    /// The margin top for the sheet, default is [`TITLE_BAR_HEIGHT`].
33    pub margin_top: Pixels,
34}
35
36impl Default for SheetSettings {
37    fn default() -> Self {
38        Self {
39            margin_top: TITLE_BAR_HEIGHT,
40        }
41    }
42}
43
44/// Sheet component that slides in from the side of the window.
45#[derive(IntoElement)]
46pub struct Sheet {
47    pub(crate) focus_handle: FocusHandle,
48    pub(crate) placement: Placement,
49    pub(crate) size: DefiniteLength,
50    resizable: bool,
51    on_close: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
52    title: Option<AnyElement>,
53    footer: Option<AnyElement>,
54    style: StyleRefinement,
55    children: Vec<AnyElement>,
56    overlay: bool,
57    overlay_closable: bool,
58}
59
60impl Sheet {
61    /// Creates a new Sheet.
62    pub fn new(_: &mut Window, cx: &mut App) -> Self {
63        Self {
64            focus_handle: cx.focus_handle(),
65            placement: Placement::Right,
66            size: DefiniteLength::Absolute(px(350.).into()),
67            resizable: true,
68            title: None,
69            footer: None,
70            style: StyleRefinement::default(),
71            children: Vec::new(),
72            overlay: true,
73            overlay_closable: true,
74            on_close: Rc::new(|_, _, _| {}),
75        }
76    }
77
78    /// Sets the title of the sheet.
79    pub fn title(mut self, title: impl IntoElement) -> Self {
80        self.title = Some(title.into_any_element());
81        self
82    }
83
84    /// Set the footer of the sheet.
85    pub fn footer(mut self, footer: impl IntoElement) -> Self {
86        self.footer = Some(footer.into_any_element());
87        self
88    }
89
90    /// Sets the size of the sheet, default is 350px.
91    pub fn size(mut self, size: impl Into<DefiniteLength>) -> Self {
92        self.size = size.into();
93        self
94    }
95
96    /// Sets whether the sheet is resizable, default is `true`.
97    pub fn resizable(mut self, resizable: bool) -> Self {
98        self.resizable = resizable;
99        self
100    }
101
102    /// Set whether the sheet should have an overlay, default is `true`.
103    pub fn overlay(mut self, overlay: bool) -> Self {
104        self.overlay = overlay;
105        self
106    }
107
108    /// Set whether the sheet should be closable by clicking the overlay, default is `true`.
109    pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
110        self.overlay_closable = overlay_closable;
111        self
112    }
113
114    /// Listen to the close event of the sheet.
115    pub fn on_close(
116        mut self,
117        on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
118    ) -> Self {
119        self.on_close = Rc::new(on_close);
120        self
121    }
122}
123
124impl EventEmitter<DismissEvent> for Sheet {}
125impl ParentElement for Sheet {
126    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
127        self.children.extend(elements);
128    }
129}
130impl Styled for Sheet {
131    fn style(&mut self) -> &mut rgpui::StyleRefinement {
132        &mut self.style
133    }
134}
135
136impl RenderOnce for Sheet {
137    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
138        let placement = self.placement;
139        let window_paddings = crate::window_border::window_paddings(window);
140        let size = window.viewport_size()
141            - rgpui::size(
142                window_paddings.left + window_paddings.right,
143                window_paddings.top + window_paddings.bottom,
144            );
145        let top = cx.theme().sheet.margin_top;
146        let on_close = self.on_close.clone();
147
148        let base_size = window.text_style().font_size;
149        let rem_size = window.rem_size();
150        let mut paddings = Edges::all(px(16.));
151        if let Some(pl) = self.style.padding.left {
152            paddings.left = pl.to_pixels(base_size, rem_size);
153        }
154        if let Some(pr) = self.style.padding.right {
155            paddings.right = pr.to_pixels(base_size, rem_size);
156        }
157        if let Some(pt) = self.style.padding.top {
158            paddings.top = pt.to_pixels(base_size, rem_size);
159        }
160        if let Some(pb) = self.style.padding.bottom {
161            paddings.bottom = pb.to_pixels(base_size, rem_size);
162        }
163
164        anchored()
165            .position(point(window_paddings.left, window_paddings.top))
166            .snap_to_window()
167            .child(
168                div()
169                    .occlude()
170                    .w(size.width)
171                    .h(size.height)
172                    .bg(overlay_color(self.overlay, cx))
173                    .when(self.overlay, |this| {
174                        this.window_control_area(WindowControlArea::Drag)
175                            .on_any_mouse_down({
176                                let on_close = self.on_close.clone();
177                                move |event, window, cx| {
178                                    if event.position.y < top {
179                                        return;
180                                    }
181
182                                    cx.stop_propagation();
183                                    if self.overlay_closable && event.button == MouseButton::Left {
184                                        window.close_sheet(cx);
185                                        on_close(&ClickEvent::default(), window, cx);
186                                    }
187                                }
188                            })
189                    })
190                    .child(
191                        v_flex()
192                            .id("sheet")
193                            .key_context(CONTEXT)
194                            .track_focus(&self.focus_handle)
195                            .focus_trap("sheet", &self.focus_handle)
196                            .on_action({
197                                let on_close = self.on_close.clone();
198                                move |_: &Cancel, window, cx| {
199                                    cx.propagate();
200
201                                    window.close_sheet(cx);
202                                    on_close(&ClickEvent::default(), window, cx);
203                                }
204                            })
205                            .absolute()
206                            .occlude()
207                            .bg(cx.theme().background)
208                            .border_color(cx.theme().border)
209                            .shadow_xl()
210                            .refine_style(&self.style)
211                            .map(|this| {
212                                // Set the size of the sheet.
213                                if placement.is_horizontal() {
214                                    this.w(self.size)
215                                } else {
216                                    this.h(self.size)
217                                }
218                            })
219                            .map(|this| match self.placement {
220                                Placement::Top => this.top(top).left_0().right_0().border_b_1(),
221                                Placement::Right => this.top(top).right_0().bottom_0().border_l_1(),
222                                Placement::Bottom => {
223                                    this.bottom_0().left_0().right_0().border_t_1()
224                                }
225                                Placement::Left => this.top(top).left_0().bottom_0().border_r_1(),
226                            })
227                            .child(
228                                // TitleBar
229                                h_flex()
230                                    .justify_between()
231                                    .pl_4()
232                                    .pr_3()
233                                    .py_2()
234                                    .w_full()
235                                    .font_semibold()
236                                    .child(self.title.unwrap_or(div().into_any_element()))
237                                    .child(
238                                        Button::new("close")
239                                            .small()
240                                            .ghost()
241                                            .icon(IconName::Close)
242                                            .on_click(move |_, window, cx| {
243                                                window.close_sheet(cx);
244                                                on_close(&ClickEvent::default(), window, cx);
245                                            }),
246                                    ),
247                            )
248                            .child(
249                                div().flex_1().overflow_hidden().child(
250                                    // Body
251                                    v_flex()
252                                        .size_full()
253                                        .overflow_y_scrollbar()
254                                        .pl(paddings.left)
255                                        .pr(paddings.right)
256                                        .children(self.children),
257                                ),
258                            )
259                            .when_some(self.footer, |this, footer| {
260                                // Footer
261                                this.child(
262                                    h_flex()
263                                        .justify_between()
264                                        .px_4()
265                                        .py_3()
266                                        .w_full()
267                                        .child(footer),
268                                )
269                            })
270                            .on_any_mouse_down({
271                                |_, _, cx| {
272                                    cx.stop_propagation();
273                                }
274                            })
275                            .with_animation(
276                                "slide",
277                                Animation::new(Duration::from_secs_f64(0.15)),
278                                move |this, delta| {
279                                    let y = px(-100.) + delta * px(100.);
280                                    this.map(|this| match placement {
281                                        Placement::Top => this.top(top + y),
282                                        Placement::Right => this.right(y),
283                                        Placement::Bottom => this.bottom(y),
284                                        Placement::Left => this.left(y),
285                                    })
286                                },
287                            ),
288                    ),
289            )
290    }
291}