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}