Skip to main content

sequoia_elements/
sheet.rs

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