Skip to main content

sequoia_view/
portal.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use sequoia_core::{Length, Rect};
6use sequoia_input::{Event, Key};
7use sequoia_layout::LayoutStyle;
8use sequoia_reactive::Signal;
9
10use crate::any::AnyView;
11use crate::overlay::{
12    Anchor, Dismiss, InputMode, OverlayEntry, OverlayId, OverlayLayer, OverlayOverflow,
13};
14use crate::view::{EventCtx, PaintCtx, View};
15use crate::views::Unit;
16
17thread_local! {
18    static PORTAL_ENTRIES: RefCell<HashMap<usize, (OverlayId, Option<crate::runtime::ElementId>)>> = RefCell::new(HashMap::new());
19    static POPPER_ENTRIES: RefCell<HashMap<usize, (OverlayId, Option<crate::runtime::ElementId>)>> = RefCell::new(HashMap::new());
20}
21
22fn get_portal_entry(key: usize) -> Option<(OverlayId, Option<crate::runtime::ElementId>)> {
23    PORTAL_ENTRIES.with(|m| m.borrow().get(&key).copied())
24}
25
26fn insert_portal_entry(key: usize, id: OverlayId, anchor: Option<crate::runtime::ElementId>) {
27    PORTAL_ENTRIES.with(|m| {
28        m.borrow_mut().insert(key, (id, anchor));
29    });
30}
31
32fn take_portal_entry(key: usize) -> Option<OverlayId> {
33    PORTAL_ENTRIES.with(|m| m.borrow_mut().remove(&key).map(|(id, _)| id))
34}
35
36fn get_popper_entry(key: usize) -> Option<(OverlayId, Option<crate::runtime::ElementId>)> {
37    POPPER_ENTRIES.with(|m| m.borrow().get(&key).copied())
38}
39
40fn insert_popper_entry(key: usize, id: OverlayId, anchor: Option<crate::runtime::ElementId>) {
41    POPPER_ENTRIES.with(|m| {
42        m.borrow_mut().insert(key, (id, anchor));
43    });
44}
45
46fn take_popper_entry(key: usize) -> Option<OverlayId> {
47    POPPER_ENTRIES.with(|m| m.borrow_mut().remove(&key).map(|(id, _)| id))
48}
49
50pub struct Portal {
51    open: Signal<bool>,
52    child: Option<AnyView>,
53    layer: OverlayLayer,
54    input_mode: InputMode,
55    dismiss: Dismiss,
56    anchor: Anchor,
57    anchor_self: bool,
58    placement: crate::overlay::Placement,
59    offset: (f32, f32),
60    overflow: OverlayOverflow,
61    on_dismiss: Option<Rc<dyn Fn()>>,
62}
63
64impl Portal {
65    pub fn new(open: Signal<bool>) -> Self {
66        Self {
67            open,
68            child: None,
69            layer: OverlayLayer::Popup,
70            input_mode: InputMode::Interactive,
71            dismiss: Dismiss::Manual,
72            anchor: Anchor::Viewport,
73            anchor_self: false,
74            placement: crate::overlay::Placement::Center,
75            offset: (0.0, 0.0),
76            overflow: OverlayOverflow::FlipAndClamp,
77            on_dismiss: None,
78        }
79    }
80
81    pub fn child<V: View + 'static>(mut self, child: V) -> Self {
82        self.child = Some(AnyView::new(child));
83        self
84    }
85
86    pub fn layer(mut self, layer: OverlayLayer) -> Self {
87        self.layer = layer;
88        self
89    }
90
91    pub fn input_mode(mut self, mode: InputMode) -> Self {
92        self.input_mode = mode;
93        self
94    }
95
96    pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
97        self.dismiss = dismiss;
98        self
99    }
100
101    pub fn anchor(mut self, anchor: Anchor) -> Self {
102        self.anchor = anchor;
103        self.anchor_self = false;
104        self
105    }
106
107    pub fn anchor_self(mut self) -> Self {
108        self.anchor_self = true;
109        self
110    }
111
112    pub fn placement(mut self, placement: crate::overlay::Placement) -> Self {
113        self.placement = placement;
114        self
115    }
116
117    pub fn offset(mut self, dx: f32, dy: f32) -> Self {
118        self.offset = (dx, dy);
119        self
120    }
121
122    pub fn overflow(mut self, overflow: OverlayOverflow) -> Self {
123        self.overflow = overflow;
124        self
125    }
126
127    pub fn on_dismiss(mut self, f: impl Fn() + 'static) -> Self {
128        self.on_dismiss = Some(Rc::new(f));
129        self
130    }
131
132    pub fn is_open(&self) -> bool {
133        self.open.get_untracked()
134    }
135
136    pub fn open(&self) {
137        self.open.set(true);
138    }
139
140    pub fn close(&self) {
141        self.open.set(false);
142    }
143
144    pub fn toggle(&self) {
145        self.open.set(!self.open.get_untracked());
146    }
147
148    fn key(&self) -> usize {
149        self.open.id()
150    }
151
152    fn sync_overlay(&self, ctx: &mut PaintCtx) {
153        let key = self.key();
154        if !self.open.get_untracked() {
155            if let Some(id) = take_portal_entry(key) {
156                ctx.hide_overlay(id);
157            }
158            return;
159        }
160        let self_anchor = if self.anchor_self {
161            Some(ctx.element)
162        } else {
163            None
164        };
165        if let Some((id, anchor)) = get_portal_entry(key) {
166            if anchor != self_anchor {
167                if ctx.has_overlay(id) {
168                    ctx.hide_overlay(id);
169                }
170                take_portal_entry(key);
171            } else {
172                if !ctx.has_overlay(id) {
173                    take_portal_entry(key);
174                    self.open.set(false);
175                    if let Some(f) = &self.on_dismiss {
176                        f();
177                    }
178                }
179                return;
180            }
181        }
182        let Some(child) = self.child.clone() else {
183            self.open.set(false);
184            return;
185        };
186        let anchor = if self.anchor_self {
187            Anchor::View(ctx.element)
188        } else {
189            self.anchor
190        };
191        let id = ctx.show_overlay(
192            OverlayEntry::new(PortalBody { child })
193                .anchor(anchor)
194                .placement(self.placement)
195                .layer(self.layer)
196                .input_mode(self.input_mode)
197                .dismiss(self.dismiss)
198                .offset(self.offset.0, self.offset.1)
199                .overflow(self.overflow),
200        );
201        insert_portal_entry(key, id, self_anchor);
202    }
203}
204
205impl View for Portal {
206    fn body(&self) -> impl View + 'static {
207        Unit
208    }
209    fn is_leaf(&self) -> bool {
210        true
211    }
212
213    fn hit_testable(&self) -> bool {
214        false
215    }
216    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
217        let _ = self.open.get();
218        Some(Vec::new())
219    }
220    fn layout_style(&self) -> LayoutStyle {
221        LayoutStyle {
222            width: Length::Fit,
223            height: Length::Fit,
224            ..LayoutStyle::default()
225        }
226    }
227    fn on_unmount(&self) {
228        take_portal_entry(self.key());
229    }
230    fn paint_self(&self, ctx: &mut PaintCtx) {
231        self.sync_overlay(ctx);
232    }
233}
234
235struct PortalBody {
236    child: AnyView,
237}
238
239impl View for PortalBody {
240    fn body(&self) -> impl View + 'static {
241        Unit
242    }
243    fn children_override(&self) -> Option<Vec<AnyView>> {
244        Some(vec![self.child.clone()])
245    }
246    fn layout_style(&self) -> LayoutStyle {
247        LayoutStyle {
248            width: Length::Fit,
249            height: Length::Fit,
250            ..LayoutStyle::default()
251        }
252    }
253}
254
255pub struct Popper {
256    open: Signal<bool>,
257    child: Option<AnyView>,
258    anchor: Anchor,
259    anchor_self: bool,
260    placement: crate::overlay::Placement,
261    offset: (f32, f32),
262    overflow: OverlayOverflow,
263    layer: OverlayLayer,
264    input_mode: InputMode,
265    dismiss: Dismiss,
266    on_dismiss: Option<Rc<dyn Fn()>>,
267}
268
269impl Popper {
270    pub fn new(open: Signal<bool>) -> Self {
271        Self {
272            open,
273            child: None,
274            anchor: Anchor::Viewport,
275            anchor_self: false,
276            placement: crate::overlay::Placement::Bottom,
277            offset: (0.0, 4.0),
278            overflow: OverlayOverflow::FlipAndClamp,
279            layer: OverlayLayer::Popup,
280            input_mode: InputMode::Interactive,
281            dismiss: Dismiss::OutsideClick,
282            on_dismiss: None,
283        }
284    }
285
286    pub fn child<V: View + 'static>(mut self, child: V) -> Self {
287        self.child = Some(AnyView::new(child));
288        self
289    }
290
291    pub fn anchor(mut self, anchor: Anchor) -> Self {
292        self.anchor = anchor;
293        self.anchor_self = false;
294        self
295    }
296
297    pub fn anchor_rect(mut self, rect: Rect) -> Self {
298        self.anchor = Anchor::Rect(rect);
299        self.anchor_self = false;
300        self
301    }
302
303    pub fn anchor_view(mut self, id: crate::runtime::ElementId) -> Self {
304        self.anchor = Anchor::View(id);
305        self.anchor_self = false;
306        self
307    }
308
309    pub fn anchor_self(mut self) -> Self {
310        self.anchor_self = true;
311        self
312    }
313
314    pub fn at_cursor(mut self) -> Self {
315        self.anchor = Anchor::Cursor;
316        self.anchor_self = false;
317        self.placement = crate::overlay::Placement::Cursor;
318        self
319    }
320
321    pub fn placement(mut self, placement: crate::overlay::Placement) -> Self {
322        self.placement = placement;
323        self
324    }
325
326    pub fn offset(mut self, dx: f32, dy: f32) -> Self {
327        self.offset = (dx, dy);
328        self
329    }
330
331    pub fn overflow(mut self, overflow: OverlayOverflow) -> Self {
332        self.overflow = overflow;
333        self
334    }
335
336    pub fn layer(mut self, layer: OverlayLayer) -> Self {
337        self.layer = layer;
338        self
339    }
340
341    pub fn input_mode(mut self, mode: InputMode) -> Self {
342        self.input_mode = mode;
343        self
344    }
345
346    pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
347        self.dismiss = dismiss;
348        self
349    }
350
351    pub fn on_dismiss(mut self, f: impl Fn() + 'static) -> Self {
352        self.on_dismiss = Some(Rc::new(f));
353        self
354    }
355
356    pub fn is_open(&self) -> bool {
357        self.open.get_untracked()
358    }
359
360    pub fn open(&self) {
361        self.open.set(true);
362    }
363
364    pub fn close(&self) {
365        self.open.set(false);
366    }
367
368    pub fn toggle(&self) {
369        self.open.set(!self.open.get_untracked());
370    }
371
372    fn key(&self) -> usize {
373        self.open.id()
374    }
375
376    fn sync_overlay(&self, ctx: &mut PaintCtx) {
377        let key = self.key();
378        if !self.open.get_untracked() {
379            if let Some(id) = take_popper_entry(key) {
380                ctx.hide_overlay(id);
381            }
382            return;
383        }
384        let self_anchor = if self.anchor_self {
385            Some(ctx.element)
386        } else {
387            None
388        };
389        if let Some((id, anchor)) = get_popper_entry(key) {
390            if anchor != self_anchor {
391                if ctx.has_overlay(id) {
392                    ctx.hide_overlay(id);
393                }
394                take_popper_entry(key);
395            } else {
396                if !ctx.has_overlay(id) {
397                    take_popper_entry(key);
398                    self.open.set(false);
399                    if let Some(f) = &self.on_dismiss {
400                        f();
401                    }
402                }
403                return;
404            }
405        }
406        let Some(child) = self.child.clone() else {
407            self.open.set(false);
408            return;
409        };
410        let anchor = if self.anchor_self {
411            Anchor::View(ctx.element)
412        } else {
413            self.anchor
414        };
415        let id = ctx.show_overlay(
416            OverlayEntry::new(PortalBody { child })
417                .anchor(anchor)
418                .placement(self.placement)
419                .layer(self.layer)
420                .input_mode(self.input_mode)
421                .dismiss(self.dismiss)
422                .offset(self.offset.0, self.offset.1)
423                .overflow(self.overflow),
424        );
425        insert_popper_entry(key, id, self_anchor);
426    }
427}
428
429impl View for Popper {
430    fn body(&self) -> impl View + 'static {
431        Unit
432    }
433    fn is_leaf(&self) -> bool {
434        true
435    }
436
437    fn hit_testable(&self) -> bool {
438        false
439    }
440    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
441        let _ = self.open.get();
442        Some(Vec::new())
443    }
444    fn layout_style(&self) -> LayoutStyle {
445        LayoutStyle {
446            width: Length::Fit,
447            height: Length::Fit,
448            ..LayoutStyle::default()
449        }
450    }
451    fn on_unmount(&self) {
452        take_popper_entry(self.key());
453    }
454    fn paint_self(&self, ctx: &mut PaintCtx) {
455        self.sync_overlay(ctx);
456    }
457}
458
459pub struct DismissableLayer {
460    child: AnyView,
461    dismiss_on_escape: bool,
462    on_dismiss: Option<Rc<dyn Fn()>>,
463    on_escape: Option<Rc<dyn Fn()>>,
464}
465
466impl DismissableLayer {
467    pub fn wrap<V: View + 'static>(child: V) -> Self {
468        Self {
469            child: AnyView::new(child),
470            dismiss_on_escape: true,
471            on_dismiss: None,
472            on_escape: None,
473        }
474    }
475
476    pub fn dismiss_on_escape(mut self, on: bool) -> Self {
477        self.dismiss_on_escape = on;
478        self
479    }
480
481    pub fn on_dismiss(mut self, f: impl Fn() + 'static) -> Self {
482        self.on_dismiss = Some(Rc::new(f));
483        self
484    }
485
486    pub fn on_escape(mut self, f: impl Fn() + 'static) -> Self {
487        self.on_escape = Some(Rc::new(f));
488        self
489    }
490
491    pub fn bind(self, open: Signal<bool>) -> Self {
492        let prev = self.on_dismiss.clone();
493        self.on_dismiss(move || {
494            if let Some(f) = &prev {
495                f();
496            }
497            open.set(false);
498        })
499    }
500
501    fn fire_escape(&self, ctx: &mut EventCtx) {
502        if let Some(f) = &self.on_escape {
503            f();
504        }
505        if self.dismiss_on_escape {
506            if let Some(f) = &self.on_dismiss {
507                f();
508            }
509            ctx.handled();
510        }
511    }
512}
513
514impl View for DismissableLayer {
515    fn body(&self) -> impl View + 'static {
516        Unit
517    }
518    fn children_override(&self) -> Option<Vec<AnyView>> {
519        Some(vec![self.child.clone()])
520    }
521    fn layout_style(&self) -> LayoutStyle {
522        LayoutStyle {
523            width: Length::Fit,
524            height: Length::Fit,
525            ..LayoutStyle::default()
526        }
527    }
528    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
529        let Event::Key(key) = event else { return };
530        if key.key != Key::ESCAPE {
531            return;
532        }
533
534        if !matches!(
535            ctx.phase,
536            sequoia_input::EventPhase::Target | sequoia_input::EventPhase::Bubble
537        ) {
538            return;
539        }
540        if !ctx.has_focus_within() && !ctx.state.focused {
541            return;
542        }
543        self.fire_escape(ctx);
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    #[test]
552    fn portal_defaults_to_manual_center_popup() {
553        let open = Signal::new(false);
554        let p = Portal::new(open);
555        assert!(matches!(p.dismiss, Dismiss::Manual));
556        assert!(matches!(p.layer, OverlayLayer::Popup));
557        assert!(matches!(p.anchor, Anchor::Viewport));
558        assert!(!p.is_open());
559    }
560
561    #[test]
562    fn popper_defaults_to_outside_click_bottom_with_gap() {
563        let open = Signal::new(true);
564        let p = Popper::new(open);
565        assert!(matches!(p.dismiss, Dismiss::OutsideClick));
566        assert!(matches!(p.placement, crate::overlay::Placement::Bottom));
567        assert_eq!(p.offset, (0.0, 4.0));
568        assert!(matches!(p.overflow, OverlayOverflow::FlipAndClamp));
569        assert!(p.is_open());
570    }
571
572    #[test]
573    fn popper_at_cursor_sets_cursor_anchor_and_placement() {
574        let open = Signal::new(false);
575        let p = Popper::new(open).at_cursor();
576        assert!(matches!(p.anchor, Anchor::Cursor));
577        assert!(matches!(p.placement, crate::overlay::Placement::Cursor));
578    }
579
580    #[test]
581    fn dismissable_binds_open_signal() {
582        let open = Signal::new(true);
583        let layer = DismissableLayer::wrap(Unit).bind(open.clone());
584        assert!(layer.dismiss_on_escape);
585
586        if let Some(f) = &layer.on_dismiss {
587            f();
588        }
589        assert!(!open.get_untracked());
590    }
591
592    #[test]
593    fn dismissable_chains_previous_on_dismiss() {
594        let open = Signal::new(true);
595        let seen = Rc::new(std::cell::Cell::new(false));
596        let seen2 = seen.clone();
597        let layer = DismissableLayer::wrap(Unit)
598            .on_dismiss(move || seen2.set(true))
599            .bind(open.clone());
600        if let Some(f) = &layer.on_dismiss {
601            f();
602        }
603        assert!(seen.get());
604        assert!(!open.get_untracked());
605    }
606}