Skip to main content

sequoia_elements/
drawer.rs

1#![forbid(unsafe_code)]
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5
6use sequoia_core::{Color, Edges, Length};
7use sequoia_layout::LayoutStyle;
8use sequoia_reactive::Signal;
9use sequoia_style::{Backdrop, DrawerStyle, Theme};
10use sequoia_view::{
11    Anchor, AnyView, Dismiss, Easing, InputMode, OverlayEntry, OverlayId, OverlayLayer,
12    OverlayOverflow, PaintCtx, Placement, Presence, Unit, View,
13};
14
15use crate::modal::{DEFAULT_SCRIM, ModalScrim, OverlayPanel};
16
17pub(crate) const DRAWER_ENTER_SECS: f32 = 0.3;
18const DRAWER_ENTER_EASE: Easing = Easing::CubicBezier(0.4, 0.0, 0.2, 1.0);
19
20const DRAWER_ENTER_X: f32 = 360.0;
21const DRAWER_ENTER_Y: f32 = 480.0;
22
23thread_local! {
24    static ENTRIES: RefCell<HashMap<usize, OverlayId>> = RefCell::new(HashMap::new());
25}
26
27fn get_entry(key: usize) -> Option<OverlayId> {
28    ENTRIES.with(|m| m.borrow().get(&key).copied())
29}
30
31fn insert_entry(key: usize, id: OverlayId) {
32    ENTRIES.with(|m| {
33        m.borrow_mut().insert(key, id);
34    });
35}
36
37fn take_entry(key: usize) -> Option<OverlayId> {
38    ENTRIES.with(|m| m.borrow_mut().remove(&key))
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum DrawerSide {
44    Top,
45    Bottom,
46    Left,
47    #[default]
48    Right,
49}
50
51impl DrawerSide {
52    fn placement(self) -> Placement {
53        match self {
54            DrawerSide::Top => Placement::Top,
55            DrawerSide::Bottom => Placement::Bottom,
56            DrawerSide::Left => Placement::Left,
57            DrawerSide::Right => Placement::Right,
58        }
59    }
60}
61
62pub struct Drawer {
63    open: Signal<bool>,
64    child: Option<AnyView>,
65    side: DrawerSide,
66    width: Length,
67    height: Length,
68    padding: Edges,
69    style: Option<DrawerStyle>,
70    background: Option<Color>,
71    border: Option<Color>,
72    border_width: Option<f32>,
73    radius: Option<f32>,
74    backdrop: Option<Backdrop>,
75    scrim: Option<Color>,
76    scrim_backdrop: Option<Backdrop>,
77    dismiss: Dismiss,
78    margin: Edges,
79}
80
81impl Drawer {
82    pub fn new(open: Signal<bool>) -> Self {
83        Self {
84            open,
85            child: None,
86            side: DrawerSide::Right,
87            width: 330.0.into(),
88            height: Length::Fit,
89            padding: Edges::ZERO,
90            style: None,
91            background: None,
92            border: None,
93            border_width: None,
94            radius: None,
95            backdrop: None,
96            scrim: None,
97            scrim_backdrop: None,
98            dismiss: Dismiss::OutsideClick,
99            margin: Edges::ZERO,
100        }
101    }
102
103    pub fn child<V: View + 'static>(mut self, child: V) -> Self {
104        self.child = Some(AnyView::new(child));
105        self
106    }
107
108    pub fn child_if<V: View + 'static>(mut self, cond: bool, make: impl FnOnce() -> V) -> Self {
109        if cond {
110            self.child = Some(AnyView::new(make()));
111        }
112        self
113    }
114
115    pub fn child_opt<V: View + 'static>(mut self, child: Option<V>) -> Self {
116        if let Some(v) = child {
117            self.child = Some(AnyView::new(v));
118        }
119        self
120    }
121
122    pub fn side(mut self, side: DrawerSide) -> Self {
123        self.side = side;
124        self
125    }
126
127    pub fn right(self) -> Self {
128        self.side(DrawerSide::Right)
129    }
130
131    pub fn left(self) -> Self {
132        self.side(DrawerSide::Left)
133    }
134
135    pub fn top(self) -> Self {
136        self.side(DrawerSide::Top)
137    }
138
139    pub fn bottom(self) -> Self {
140        self.side(DrawerSide::Bottom)
141    }
142
143    pub fn width(mut self, width: impl Into<Length>) -> Self {
144        self.width = width.into();
145        self
146    }
147
148    pub fn height(mut self, height: impl Into<Length>) -> Self {
149        self.height = height.into();
150        self
151    }
152
153    pub fn size(mut self, width: impl Into<Length>, height: impl Into<Length>) -> Self {
154        self.width = width.into();
155        self.height = height.into();
156        self
157    }
158
159    pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
160        self.padding = padding.into();
161        self
162    }
163
164    pub fn pad(self, padding: impl Into<Edges>) -> Self {
165        self.padding(padding)
166    }
167
168    pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
169        self.padding = Edges::xy(x, y);
170        self
171    }
172
173    pub fn style(mut self, style: DrawerStyle) -> Self {
174        self.style = Some(style);
175        self
176    }
177
178    pub fn background(mut self, color: Color) -> Self {
179        self.background = Some(color);
180        self
181    }
182
183    pub fn border_color(mut self, color: Color) -> Self {
184        self.border = Some(color);
185        self
186    }
187
188    pub fn border_width(mut self, width: f32) -> Self {
189        self.border_width = Some(width.max(0.0));
190        self
191    }
192
193    pub fn border(mut self, width: f32, color: Color) -> Self {
194        self.border_width = Some(width.max(0.0));
195        self.border = Some(color);
196        self
197    }
198
199    pub fn radius(mut self, radius: f32) -> Self {
200        self.radius = Some(radius.max(0.0));
201        self
202    }
203
204    pub fn backdrop(mut self, backdrop: Backdrop) -> Self {
205        self.backdrop = Some(backdrop);
206        self
207    }
208
209    pub fn scrim_color(mut self, color: Color) -> Self {
210        self.scrim = Some(color);
211        self
212    }
213
214    pub fn scrim_backdrop(mut self, backdrop: Backdrop) -> Self {
215        self.scrim_backdrop = Some(backdrop);
216        self
217    }
218
219    pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
220        self.dismiss = dismiss;
221        self
222    }
223
224    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
225        let m = margin.into();
226        self.margin = Edges::only(
227            sanitize_margin(m.left),
228            sanitize_margin(m.top),
229            sanitize_margin(m.right),
230            sanitize_margin(m.bottom),
231        );
232        self
233    }
234
235    pub fn margin_xy(self, x: f32, y: f32) -> Self {
236        self.margin(Edges::xy(x, y))
237    }
238
239    pub fn margin_x(mut self, x: f32) -> Self {
240        let x = sanitize_margin(x);
241        self.margin.left = x;
242        self.margin.right = x;
243        self
244    }
245
246    pub fn margin_y(mut self, y: f32) -> Self {
247        let y = sanitize_margin(y);
248        self.margin.top = y;
249        self.margin.bottom = y;
250        self
251    }
252
253    pub fn margin_left(mut self, v: f32) -> Self {
254        self.margin.left = sanitize_margin(v);
255        self
256    }
257
258    pub fn margin_right(mut self, v: f32) -> Self {
259        self.margin.right = sanitize_margin(v);
260        self
261    }
262
263    pub fn margin_top(mut self, v: f32) -> Self {
264        self.margin.top = sanitize_margin(v);
265        self
266    }
267
268    pub fn margin_bottom(mut self, v: f32) -> Self {
269        self.margin.bottom = sanitize_margin(v);
270        self
271    }
272
273    pub fn scrim(&self) -> ModalScrim {
274        let style = self.style.unwrap_or_default();
275        ModalScrim::new(self.open.clone())
276            .scrim_color(self.scrim.or(style.scrim).unwrap_or(DEFAULT_SCRIM))
277            .backdrop(
278                self.scrim_backdrop
279                    .or(style.backdrop)
280                    .unwrap_or(Backdrop::NONE),
281            )
282    }
283
284    pub fn is_open(&self) -> bool {
285        self.open.get_untracked()
286    }
287
288    pub fn open(&self) {
289        self.open.set(true);
290    }
291
292    pub fn close(&self) {
293        self.open.set(false);
294    }
295
296    pub fn toggle(&self) {
297        self.open.set(!self.open.get_untracked());
298    }
299
300    fn key(&self) -> usize {
301        self.open.id()
302    }
303
304    fn sync_overlay(&self, ctx: &mut PaintCtx, theme: &Theme) {
305        let key = self.key();
306        if !self.open.get_untracked() {
307            if let Some(id) = take_entry(key) {
308                ctx.hide_overlay(id);
309            }
310            return;
311        }
312        if let Some(id) = get_entry(key) {
313            if !ctx.has_overlay(id) {
314                take_entry(key);
315                self.open.set(false);
316            }
317            return;
318        }
319        let Some(child) = self.child.clone() else {
320            self.open.set(false);
321            return;
322        };
323        let style = self.style.unwrap_or_default();
324        let radius = self.radius.or(style.radius).unwrap_or(theme.radius);
325        let backdrop = self.backdrop.or(style.backdrop).unwrap_or(Backdrop::MD);
326        let panel = OverlayPanel {
327            child,
328            width: self.width,
329            height: self.height,
330            padding: self.padding,
331            background: self
332                .background
333                .or(style.background)
334                .unwrap_or(theme.surface),
335            border: self.border.or(style.border).unwrap_or(theme.border),
336            border_width: self
337                .border_width
338                .or(style.border_width)
339                .unwrap_or(1.0)
340                .max(0.0),
341            radius,
342            backdrop,
343        };
344
345        let (dx, dy) = match self.side {
346            DrawerSide::Left => (-DRAWER_ENTER_X, 0.0),
347            DrawerSide::Right => (DRAWER_ENTER_X, 0.0),
348            DrawerSide::Top => (0.0, -DRAWER_ENTER_Y),
349            DrawerSide::Bottom => (0.0, DRAWER_ENTER_Y),
350        };
351
352        let enter = Presence::new(Signal::new(true))
353            .from(1.0)
354            .enter_duration(DRAWER_ENTER_SECS)
355            .enter_easing(DRAWER_ENTER_EASE)
356            .slide(dx, dy)
357            .child(panel);
358        let id = ctx.show_overlay(
359            OverlayEntry::new(enter)
360                .anchor(Anchor::Viewport)
361                .placement(self.side.placement())
362                .layer(OverlayLayer::Modal)
363                .input_mode(InputMode::Modal)
364                .dismiss(self.dismiss)
365                .overflow(OverlayOverflow::Clamp),
366        );
367        insert_entry(key, id);
368    }
369}
370
371impl View for Drawer {
372    fn body(&self) -> impl View + 'static {
373        Unit
374    }
375    fn is_leaf(&self) -> bool {
376        true
377    }
378
379    fn hit_testable(&self) -> bool {
380        false
381    }
382    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
383        let _ = self.open.get();
384        Some(Vec::new())
385    }
386    fn layout_style(&self) -> LayoutStyle {
387        LayoutStyle {
388            width: Length::Fit,
389            height: Length::Fit,
390            margin: self.margin,
391            ..LayoutStyle::default()
392        }
393    }
394    fn on_unmount(&self) {
395        take_entry(self.key());
396    }
397    fn paint_self(&self, ctx: &mut PaintCtx) {
398        let theme = ctx.theme();
399        self.sync_overlay(ctx, &theme);
400    }
401}
402
403fn sanitize_margin(v: f32) -> f32 {
404    if v.is_finite() { v.max(0.0) } else { 0.0 }
405}