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 {
318 self.anchor_self = true;
319 self
320 }
321
322 pub fn at_cursor(mut self) -> Self {
323 self.anchor = Anchor::Cursor;
324 self.anchor_self = false;
325 self.placement = crate::overlay::Placement::Cursor;
326 self
327 }
328
329 pub fn anchor_point(mut self, x: f32, y: f32) -> Self {
336 self.anchor = Anchor::Point(sequoia_core::Point::new(x, y));
337 self.anchor_self = false;
338 self.placement = crate::overlay::Placement::Cursor;
339 self
340 }
341
342 pub fn anchor_static(self, point: sequoia_core::Point) -> Self {
344 self.anchor_point(point.x, point.y)
345 }
346
347 pub fn placement(mut self, placement: crate::overlay::Placement) -> Self {
348 self.placement = placement;
349 self
350 }
351
352 pub fn offset(mut self, dx: f32, dy: f32) -> Self {
353 self.offset = (dx, dy);
354 self
355 }
356
357 pub fn overflow(mut self, overflow: OverlayOverflow) -> Self {
358 self.overflow = overflow;
359 self
360 }
361
362 pub fn layer(mut self, layer: OverlayLayer) -> Self {
363 self.layer = layer;
364 self
365 }
366
367 pub fn input_mode(mut self, mode: InputMode) -> Self {
368 self.input_mode = mode;
369 self
370 }
371
372 pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
373 self.dismiss = dismiss;
374 self
375 }
376
377 pub fn on_dismiss(mut self, f: impl Fn() + 'static) -> Self {
378 self.on_dismiss = Some(Rc::new(f));
379 self
380 }
381
382 pub fn is_open(&self) -> bool {
383 self.open.get_untracked()
384 }
385
386 pub fn open(&self) {
387 self.open.set(true);
388 }
389
390 pub fn close(&self) {
391 self.open.set(false);
392 }
393
394 pub fn toggle(&self) {
395 self.open.set(!self.open.get_untracked());
396 }
397
398 fn key(&self) -> usize {
399 self.open.id()
400 }
401
402 fn sync_overlay(&self, ctx: &mut PaintCtx) {
403 let key = self.key();
404 if !self.open.get_untracked() {
405 if let Some(id) = take_popper_entry(key) {
406 ctx.hide_overlay(id);
407 }
408 return;
409 }
410 let self_anchor = if self.anchor_self {
411 Some(ctx.element)
412 } else {
413 None
414 };
415 if let Some((id, anchor)) = get_popper_entry(key) {
416 if anchor != self_anchor {
417 if ctx.has_overlay(id) {
418 ctx.hide_overlay(id);
419 }
420 take_popper_entry(key);
421 } else {
422 if !ctx.has_overlay(id) {
423 take_popper_entry(key);
424 self.open.set(false);
425 if let Some(f) = &self.on_dismiss {
426 f();
427 }
428 }
429 return;
430 }
431 }
432 let Some(child) = self.child.clone() else {
433 self.open.set(false);
434 return;
435 };
436 let anchor = if self.anchor_self {
437 Anchor::View(ctx.element)
438 } else {
439 self.anchor
440 };
441 let id = ctx.show_overlay(
442 OverlayEntry::new(PortalBody { child })
443 .anchor(anchor)
444 .placement(self.placement)
445 .layer(self.layer)
446 .input_mode(self.input_mode)
447 .dismiss(self.dismiss)
448 .offset(self.offset.0, self.offset.1)
449 .overflow(self.overflow),
450 );
451 insert_popper_entry(key, id, self_anchor);
452 }
453}
454
455impl View for Popper {
456 fn body(&self) -> impl View + 'static {
457 Unit
458 }
459 fn is_leaf(&self) -> bool {
460 true
461 }
462
463 fn hit_testable(&self) -> bool {
464 false
465 }
466 fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
467 let _ = self.open.get();
468 Some(Vec::new())
469 }
470 fn layout_style(&self) -> LayoutStyle {
471 LayoutStyle {
472 width: Length::Fit,
473 height: Length::Fit,
474 ..LayoutStyle::default()
475 }
476 }
477 fn on_unmount(&self) {
478 take_popper_entry(self.key());
479 }
480 fn paint_self(&self, ctx: &mut PaintCtx) {
481 self.sync_overlay(ctx);
482 }
483}
484
485pub struct DismissableLayer {
486 child: AnyView,
487 dismiss_on_escape: bool,
488 on_dismiss: Option<Rc<dyn Fn()>>,
489 on_escape: Option<Rc<dyn Fn()>>,
490}
491
492impl DismissableLayer {
493 pub fn wrap<V: View + 'static>(child: V) -> Self {
494 Self {
495 child: AnyView::new(child),
496 dismiss_on_escape: true,
497 on_dismiss: None,
498 on_escape: None,
499 }
500 }
501
502 pub fn dismiss_on_escape(mut self, on: bool) -> Self {
503 self.dismiss_on_escape = on;
504 self
505 }
506
507 pub fn on_dismiss(mut self, f: impl Fn() + 'static) -> Self {
508 self.on_dismiss = Some(Rc::new(f));
509 self
510 }
511
512 pub fn on_escape(mut self, f: impl Fn() + 'static) -> Self {
513 self.on_escape = Some(Rc::new(f));
514 self
515 }
516
517 pub fn bind(self, open: Signal<bool>) -> Self {
518 let prev = self.on_dismiss.clone();
519 self.on_dismiss(move || {
520 if let Some(f) = &prev {
521 f();
522 }
523 open.set(false);
524 })
525 }
526
527 fn fire_escape(&self, ctx: &mut EventCtx) {
528 if let Some(f) = &self.on_escape {
529 f();
530 }
531 if self.dismiss_on_escape {
532 if let Some(f) = &self.on_dismiss {
533 f();
534 }
535 ctx.handled();
536 }
537 }
538}
539
540impl View for DismissableLayer {
541 fn body(&self) -> impl View + 'static {
542 Unit
543 }
544 fn children_override(&self) -> Option<Vec<AnyView>> {
545 Some(vec![self.child.clone()])
546 }
547 fn layout_style(&self) -> LayoutStyle {
548 LayoutStyle {
549 width: Length::Fit,
550 height: Length::Fit,
551 ..LayoutStyle::default()
552 }
553 }
554 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
555 let Event::Key(key) = event else { return };
556 if key.key != Key::ESCAPE {
557 return;
558 }
559
560 if !matches!(
561 ctx.phase,
562 sequoia_input::EventPhase::Target | sequoia_input::EventPhase::Bubble
563 ) {
564 return;
565 }
566 if !ctx.has_focus_within() && !ctx.state.focused {
567 return;
568 }
569 self.fire_escape(ctx);
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn portal_defaults_to_manual_center_popup() {
579 let open = Signal::new(false);
580 let p = Portal::new(open);
581 assert!(matches!(p.dismiss, Dismiss::Manual));
582 assert!(matches!(p.layer, OverlayLayer::Popup));
583 assert!(matches!(p.anchor, Anchor::Viewport));
584 assert!(!p.is_open());
585 }
586
587 #[test]
588 fn popper_defaults_to_outside_click_bottom_with_gap() {
589 let open = Signal::new(true);
590 let p = Popper::new(open);
591 assert!(matches!(p.dismiss, Dismiss::OutsideClick));
592 assert!(matches!(p.placement, crate::overlay::Placement::Bottom));
593 assert_eq!(p.offset, (0.0, 4.0));
594 assert!(matches!(p.overflow, OverlayOverflow::FlipAndClamp));
595 assert!(p.is_open());
596 }
597
598 #[test]
599 fn popper_at_cursor_sets_cursor_anchor_and_placement() {
600 let open = Signal::new(false);
601 let p = Popper::new(open).at_cursor();
602 assert!(matches!(p.anchor, Anchor::Cursor));
603 assert!(matches!(p.placement, crate::overlay::Placement::Cursor));
604 }
605
606 #[test]
607 fn popper_anchor_point_stays_static() {
608 let open = Signal::new(false);
609 let p = Popper::new(open).anchor_point(12.0, 34.0);
610 assert!(
611 matches!(p.anchor, Anchor::Point(pt) if pt == sequoia_core::Point::new(12.0, 34.0))
612 );
613 assert!(matches!(p.placement, crate::overlay::Placement::Cursor));
614
615 let p = Popper::new(Signal::new(false)).anchor_static(sequoia_core::Point::new(5.0, 6.0));
616 assert!(matches!(p.anchor, Anchor::Point(pt) if pt == sequoia_core::Point::new(5.0, 6.0)));
617 }
618
619 #[test]
620 fn dismissable_binds_open_signal() {
621 let open = Signal::new(true);
622 let layer = DismissableLayer::wrap(Unit).bind(open.clone());
623 assert!(layer.dismiss_on_escape);
624
625 if let Some(f) = &layer.on_dismiss {
626 f();
627 }
628 assert!(!open.get_untracked());
629 }
630
631 #[test]
632 fn dismissable_chains_previous_on_dismiss() {
633 let open = Signal::new(true);
634 let seen = Rc::new(std::cell::Cell::new(false));
635 let seen2 = seen.clone();
636 let layer = DismissableLayer::wrap(Unit)
637 .on_dismiss(move || seen2.set(true))
638 .bind(open.clone());
639 if let Some(f) = &layer.on_dismiss {
640 f();
641 }
642 assert!(seen.get());
643 assert!(!open.get_untracked());
644 }
645}