Skip to main content

sequoia_view/
overlay.rs

1use std::cell::Cell;
2
3use sequoia_core::{Point, Rect, Size};
4
5use crate::any::AnyView;
6use crate::runtime::ElementId;
7use crate::view::View;
8
9#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
10pub struct OverlayId(pub(crate) u64);
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
13#[non_exhaustive]
14pub enum OverlayLayer {
15    Tooltip,
16
17    Popup,
18
19    Menu,
20
21    DragPreview,
22
23    Modal,
24
25    System,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
29#[non_exhaustive]
30pub enum InputMode {
31    Passthrough,
32
33    #[default]
34    Interactive,
35
36    Modal,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
40#[non_exhaustive]
41pub enum Dismiss {
42    #[default]
43    Manual,
44
45    Escape,
46
47    OutsideClick,
48
49    FocusLost,
50}
51
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum OverlayOverflow {
55    #[default]
56    FlipAndClamp,
57
58    Clamp,
59
60    Visible,
61}
62
63#[derive(Clone, Copy, Debug, Default)]
64#[non_exhaustive]
65pub enum Anchor {
66    View(ElementId),
67
68    Rect(Rect),
69
70    Point(Point),
71
72    Cursor,
73
74    #[default]
75    Viewport,
76}
77
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum Position {
81    #[default]
82    Absolute,
83
84    Relative,
85
86    Fixed,
87}
88
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum Placement {
92    Top,
93
94    TopStart,
95
96    TopEnd,
97
98    #[default]
99    Bottom,
100
101    BottomStart,
102
103    BottomEnd,
104
105    Left,
106
107    Right,
108
109    Center,
110
111    Cursor,
112}
113
114pub struct OverlayEntry {
115    pub(crate) id: OverlayId,
116    pub(crate) view: AnyView,
117    pub(crate) anchor: Anchor,
118    pub(crate) placement: Placement,
119    pub(crate) position: Position,
120    pub(crate) overflow: OverlayOverflow,
121    pub(crate) layer: OverlayLayer,
122    pub(crate) input_mode: InputMode,
123    pub(crate) dismiss: Dismiss,
124
125    pub(crate) transient: bool,
126    pub(crate) offset: (f32, f32),
127    pub(crate) element: Cell<Option<ElementId>>,
128    pub(crate) rect: Cell<Rect>,
129    pub(crate) owner: Option<ElementId>,
130}
131
132impl OverlayEntry {
133    pub fn new(view: impl View + 'static) -> Self {
134        Self {
135            id: OverlayId(0),
136            view: AnyView::new(view),
137            anchor: Anchor::default(),
138            placement: Placement::default(),
139            position: Position::default(),
140            overflow: OverlayOverflow::default(),
141            layer: OverlayLayer::Popup,
142            input_mode: InputMode::default(),
143            dismiss: Dismiss::default(),
144            transient: false,
145            offset: (0.0, 0.0),
146            element: Cell::new(None),
147            rect: Cell::new(Rect::ZERO),
148            owner: None,
149        }
150    }
151
152    pub(crate) fn owned_by(mut self, owner: ElementId) -> Self {
153        self.owner = Some(owner);
154        self
155    }
156
157    pub fn anchor(mut self, anchor: Anchor) -> Self {
158        self.anchor = anchor;
159        self
160    }
161
162    pub fn placement(mut self, placement: Placement) -> Self {
163        self.placement = placement;
164        self
165    }
166
167    pub fn position(mut self, position: Position) -> Self {
168        self.position = position;
169        self
170    }
171
172    pub fn at(mut self, x: f32, y: f32) -> Self {
173        self.position = Position::Fixed;
174        self.offset = (x, y);
175        self
176    }
177
178    pub fn layer(mut self, layer: OverlayLayer) -> Self {
179        self.layer = layer;
180        self
181    }
182
183    pub fn input_mode(mut self, mode: InputMode) -> Self {
184        self.input_mode = mode;
185        self
186    }
187
188    pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
189        self.dismiss = dismiss;
190        self
191    }
192
193    pub fn transient(mut self, transient: bool) -> Self {
194        self.transient = transient;
195        self
196    }
197
198    pub fn offset(mut self, dx: f32, dy: f32) -> Self {
199        self.offset = (dx, dy);
200        self
201    }
202
203    pub fn overflow(mut self, overflow: OverlayOverflow) -> Self {
204        self.overflow = overflow;
205        self
206    }
207}
208
209#[derive(Default)]
210pub(crate) struct OverlayStore {
211    entries: Vec<OverlayEntry>,
212    next_id: u64,
213    anchor_index: std::collections::HashMap<crate::runtime::ElementId, Vec<OverlayId>>,
214}
215
216impl OverlayStore {
217    pub(crate) fn insert(&mut self, mut entry: OverlayEntry) -> OverlayId {
218        let id = OverlayId(self.next_id);
219        self.next_id += 1;
220        entry.id = id;
221        if let Anchor::View(a) = entry.anchor {
222            self.anchor_index.entry(a).or_default().push(id);
223        }
224        self.entries.push(entry);
225        id
226    }
227
228    pub(crate) fn get(&self, id: OverlayId) -> Option<&OverlayEntry> {
229        self.entries.iter().find(|e| e.id == id)
230    }
231
232    pub(crate) fn remove(&mut self, id: OverlayId) -> Option<OverlayEntry> {
233        let pos = self.entries.iter().position(|e| e.id == id)?;
234        let entry = self.entries.remove(pos);
235        if let Anchor::View(a) = entry.anchor
236            && let Some(v) = self.anchor_index.get_mut(&a)
237        {
238            v.retain(|&x| x != id);
239            if v.is_empty() {
240                self.anchor_index.remove(&a);
241            }
242        }
243        Some(entry)
244    }
245
246    pub(crate) fn ordered_ids(&self) -> Vec<OverlayId> {
247        let mut ids: Vec<(OverlayLayer, usize, OverlayId)> = self
248            .entries
249            .iter()
250            .enumerate()
251            .map(|(i, e)| (e.layer, i, e.id))
252            .collect();
253        ids.sort_by_key(|(l, i, _)| (*l, *i));
254        ids.into_iter().map(|(_, _, id)| id).collect()
255    }
256
257    pub(crate) fn transient_ids(&self) -> Vec<OverlayId> {
258        self.entries
259            .iter()
260            .filter(|e| e.transient)
261            .map(|e| e.id)
262            .collect()
263    }
264
265    pub(crate) fn anchored_to(&self, el: crate::runtime::ElementId) -> Vec<OverlayId> {
266        let mut ids = self.anchor_index.get(&el).cloned().unwrap_or_else(|| {
267            self.entries
268                .iter()
269                .filter(|e| matches!(e.anchor, Anchor::View(a) if a == el))
270                .map(|e| e.id)
271                .collect()
272        });
273        for entry in &self.entries {
274            if entry.owner == Some(el) && !ids.contains(&entry.id) {
275                ids.push(entry.id);
276            }
277        }
278        ids
279    }
280
281    pub(crate) fn outside_dismiss_ids(&self, pos: Point) -> Vec<OverlayId> {
282        self.entries
283            .iter()
284            .filter(|e| e.dismiss == Dismiss::OutsideClick && !e.rect.get().contains(pos))
285            .map(|e| e.id)
286            .collect()
287    }
288
289    pub(crate) fn escape_dismiss_id(&self) -> Option<OverlayId> {
290        self.ordered_ids()
291            .into_iter()
292            .rev()
293            .find(|id| self.get(*id).is_some_and(|e| e.dismiss == Dismiss::Escape))
294    }
295
296    pub(crate) fn focus_lost_ids(
297        &self,
298        target: Option<ElementId>,
299        inside: impl Fn(Option<ElementId>) -> bool,
300    ) -> Vec<OverlayId> {
301        self.entries
302            .iter()
303            .filter(|e| {
304                if e.dismiss != Dismiss::FocusLost {
305                    return false;
306                }
307                match target {
308                    None => true,
309                    Some(_) => !inside(e.element.get()),
310                }
311            })
312            .map(|e| e.id)
313            .collect()
314    }
315
316    pub(crate) fn len(&self) -> usize {
317        self.entries.len()
318    }
319
320    pub(crate) fn set_offset(&mut self, id: OverlayId, offset: (f32, f32)) -> bool {
321        match self.entries.iter_mut().find(|e| e.id == id) {
322            Some(e) => {
323                e.offset = offset;
324                true
325            }
326            None => false,
327        }
328    }
329}
330
331pub(crate) fn compute_position(
332    position: Position,
333    placement: Placement,
334    anchor: Rect,
335    size: Size,
336    viewport: Rect,
337    offset: (f32, f32),
338    overflow: OverlayOverflow,
339) -> Point {
340    match position {
341        Position::Absolute => {
342            placement_position(placement, anchor, size, viewport, offset, overflow)
343        }
344        Position::Relative => clamp_to_viewport(
345            Point::new(anchor.left() + offset.0, anchor.top() + offset.1),
346            size,
347            viewport,
348        ),
349        Position::Fixed => {
350            let pos = Point::new(viewport.left() + offset.0, viewport.top() + offset.1);
351            match overflow {
352                OverlayOverflow::Visible => pos,
353                _ => clamp_to_viewport(pos, size, viewport),
354            }
355        }
356    }
357}
358
359fn clamp_to_viewport(pos: Point, size: Size, viewport: Rect) -> Point {
360    let max_x = (viewport.right() - size.width).max(viewport.left());
361    let max_y = (viewport.bottom() - size.height).max(viewport.top());
362    Point::new(
363        pos.x.clamp(viewport.left(), max_x),
364        pos.y.clamp(viewport.top(), max_y),
365    )
366}
367
368pub(crate) fn placement_position(
369    placement: Placement,
370    anchor: Rect,
371    size: Size,
372    viewport: Rect,
373    offset: (f32, f32),
374    overflow: OverlayOverflow,
375) -> Point {
376    let placement = match overflow {
377        OverlayOverflow::FlipAndClamp => flip_if_needed(placement, anchor, size, viewport),
378        _ => placement,
379    };
380    let (ox, oy) = offset;
381    let (mut x, mut y) = match placement {
382        Placement::Top => (
383            anchor.left() + (anchor.size.width - size.width) * 0.5,
384            anchor.top() - size.height,
385        ),
386        Placement::TopStart => (anchor.left(), anchor.top() - size.height),
387        Placement::TopEnd => (anchor.right() - size.width, anchor.top() - size.height),
388        Placement::Bottom => (
389            anchor.left() + (anchor.size.width - size.width) * 0.5,
390            anchor.bottom(),
391        ),
392        Placement::BottomStart => (anchor.left(), anchor.bottom()),
393        Placement::BottomEnd => (anchor.right() - size.width, anchor.bottom()),
394        Placement::Left => (
395            anchor.left() - size.width,
396            anchor.top() + (anchor.size.height - size.height) * 0.5,
397        ),
398        Placement::Right => (
399            anchor.right(),
400            anchor.top() + (anchor.size.height - size.height) * 0.5,
401        ),
402        Placement::Center => (
403            anchor.left() + (anchor.size.width - size.width) * 0.5,
404            anchor.top() + (anchor.size.height - size.height) * 0.5,
405        ),
406        Placement::Cursor => (anchor.left(), anchor.top()),
407    };
408    x += ox;
409    y += oy;
410
411    match overflow {
412        OverlayOverflow::Visible => Point::new(x, y),
413        _ => clamp_to_viewport(Point::new(x, y), size, viewport),
414    }
415}
416
417fn flip_if_needed(placement: Placement, anchor: Rect, size: Size, viewport: Rect) -> Placement {
418    match placement {
419        Placement::Bottom | Placement::BottomStart | Placement::BottomEnd => {
420            let fits_below = anchor.bottom() + size.height <= viewport.bottom();
421            let fits_above =
422                anchor.top() - size.height >= viewport.top() && size.height <= viewport.size.height;
423            if !fits_below && fits_above {
424                match placement {
425                    Placement::BottomStart => Placement::TopStart,
426                    Placement::BottomEnd => Placement::TopEnd,
427                    _ => Placement::Top,
428                }
429            } else {
430                placement
431            }
432        }
433        Placement::Top | Placement::TopStart | Placement::TopEnd => {
434            let fits_above = anchor.top() - size.height >= viewport.top();
435            let fits_below = anchor.bottom() + size.height <= viewport.bottom()
436                && size.height <= viewport.size.height;
437            if !fits_above && fits_below {
438                match placement {
439                    Placement::TopStart => Placement::BottomStart,
440                    Placement::TopEnd => Placement::BottomEnd,
441                    _ => Placement::Bottom,
442                }
443            } else {
444                placement
445            }
446        }
447        Placement::Right => {
448            let fits_right = anchor.right() + size.width <= viewport.right();
449            let fits_left =
450                anchor.left() - size.width >= viewport.left() && size.width <= viewport.size.width;
451            if !fits_right && fits_left {
452                Placement::Left
453            } else {
454                placement
455            }
456        }
457        Placement::Left => {
458            let fits_left = anchor.left() - size.width >= viewport.left();
459            let fits_right = anchor.right() + size.width <= viewport.right()
460                && size.width <= viewport.size.width;
461            if !fits_left && fits_right {
462                Placement::Right
463            } else {
464                placement
465            }
466        }
467        Placement::Center | Placement::Cursor => placement,
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    fn viewport() -> Rect {
476        Rect::from_xywh(0.0, 0.0, 800.0, 600.0)
477    }
478
479    #[test]
480    fn bottom_flips_to_top_when_it_does_not_fit() {
481        let anchor = Rect::from_xywh(100.0, 500.0, 120.0, 32.0);
482        let size = Size::new(200.0, 200.0);
483        let p = placement_position(
484            Placement::Bottom,
485            anchor,
486            size,
487            viewport(),
488            (0.0, 4.0),
489            OverlayOverflow::FlipAndClamp,
490        );
491        assert!(
492            p.y + size.height <= anchor.top() + 5.0,
493            "flipped above the anchor (plus 4px offset): {p:?}"
494        );
495    }
496
497    #[test]
498    fn clamp_keeps_placement_but_trims_to_viewport() {
499        let anchor = Rect::from_xywh(100.0, 500.0, 120.0, 32.0);
500        let size = Size::new(200.0, 200.0);
501        let p = placement_position(
502            Placement::Bottom,
503            anchor,
504            size,
505            viewport(),
506            (0.0, 4.0),
507            OverlayOverflow::Clamp,
508        );
509
510        assert!(
511            (p.y - 400.0).abs() < 1e-4,
512            "clamped to viewport without flip: {p:?}"
513        );
514        assert!(
515            p.y + size.height <= viewport().bottom() + 1.0,
516            "fits vertically: {p:?}"
517        );
518    }
519
520    #[test]
521    fn visible_may_bleed_outside_viewport() {
522        let anchor = Rect::from_xywh(100.0, 500.0, 120.0, 32.0);
523        let size = Size::new(200.0, 200.0);
524        let p = placement_position(
525            Placement::Bottom,
526            anchor,
527            size,
528            viewport(),
529            (0.0, 4.0),
530            OverlayOverflow::Visible,
531        );
532        assert!(
533            (p.y - (anchor.bottom() + 4.0)).abs() < 1e-4,
534            "exactly under the anchor, no fitting: {p:?}"
535        );
536        assert!(p.y + size.height > viewport().bottom(), "sticks out");
537    }
538
539    #[test]
540    fn fitting_popup_is_unaffected_by_policy() {
541        let anchor = Rect::from_xywh(100.0, 100.0, 120.0, 32.0);
542        let size = Size::new(200.0, 120.0);
543        for policy in [
544            OverlayOverflow::FlipAndClamp,
545            OverlayOverflow::Clamp,
546            OverlayOverflow::Visible,
547        ] {
548            let p = placement_position(
549                Placement::Bottom,
550                anchor,
551                size,
552                viewport(),
553                (0.0, 4.0),
554                policy,
555            );
556            assert!(
557                (p.y - (anchor.bottom() + 4.0)).abs() < 1e-4,
558                "{policy:?}: unchanged when it fits: {p:?}"
559            );
560        }
561    }
562}