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}