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}
130
131impl OverlayEntry {
132 pub fn new(view: impl View + 'static) -> Self {
133 Self {
134 id: OverlayId(0),
135 view: AnyView::new(view),
136 anchor: Anchor::default(),
137 placement: Placement::default(),
138 position: Position::default(),
139 overflow: OverlayOverflow::default(),
140 layer: OverlayLayer::Popup,
141 input_mode: InputMode::default(),
142 dismiss: Dismiss::default(),
143 transient: false,
144 offset: (0.0, 0.0),
145 element: Cell::new(None),
146 rect: Cell::new(Rect::ZERO),
147 }
148 }
149
150 pub fn anchor(mut self, anchor: Anchor) -> Self {
151 self.anchor = anchor;
152 self
153 }
154
155 pub fn placement(mut self, placement: Placement) -> Self {
156 self.placement = placement;
157 self
158 }
159
160 pub fn position(mut self, position: Position) -> Self {
161 self.position = position;
162 self
163 }
164
165 pub fn at(mut self, x: f32, y: f32) -> Self {
166 self.position = Position::Fixed;
167 self.offset = (x, y);
168 self
169 }
170
171 pub fn layer(mut self, layer: OverlayLayer) -> Self {
172 self.layer = layer;
173 self
174 }
175
176 pub fn input_mode(mut self, mode: InputMode) -> Self {
177 self.input_mode = mode;
178 self
179 }
180
181 pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
182 self.dismiss = dismiss;
183 self
184 }
185
186 pub fn transient(mut self, transient: bool) -> Self {
187 self.transient = transient;
188 self
189 }
190
191 pub fn offset(mut self, dx: f32, dy: f32) -> Self {
192 self.offset = (dx, dy);
193 self
194 }
195
196 pub fn overflow(mut self, overflow: OverlayOverflow) -> Self {
197 self.overflow = overflow;
198 self
199 }
200}
201
202#[derive(Default)]
203pub(crate) struct OverlayStore {
204 entries: Vec<OverlayEntry>,
205 next_id: u64,
206 anchor_index: std::collections::HashMap<crate::runtime::ElementId, Vec<OverlayId>>,
207}
208
209impl OverlayStore {
210 pub(crate) fn insert(&mut self, mut entry: OverlayEntry) -> OverlayId {
211 let id = OverlayId(self.next_id);
212 self.next_id += 1;
213 entry.id = id;
214 if let Anchor::View(a) = entry.anchor {
215 self.anchor_index.entry(a).or_default().push(id);
216 }
217 self.entries.push(entry);
218 id
219 }
220
221 pub(crate) fn get(&self, id: OverlayId) -> Option<&OverlayEntry> {
222 self.entries.iter().find(|e| e.id == id)
223 }
224
225 pub(crate) fn remove(&mut self, id: OverlayId) -> Option<OverlayEntry> {
226 let pos = self.entries.iter().position(|e| e.id == id)?;
227 let entry = self.entries.remove(pos);
228 if let Anchor::View(a) = entry.anchor
229 && let Some(v) = self.anchor_index.get_mut(&a)
230 {
231 v.retain(|&x| x != id);
232 if v.is_empty() {
233 self.anchor_index.remove(&a);
234 }
235 }
236 Some(entry)
237 }
238
239 pub(crate) fn ordered_ids(&self) -> Vec<OverlayId> {
240 let mut ids: Vec<(OverlayLayer, usize, OverlayId)> = self
241 .entries
242 .iter()
243 .enumerate()
244 .map(|(i, e)| (e.layer, i, e.id))
245 .collect();
246 ids.sort_by_key(|(l, i, _)| (*l, *i));
247 ids.into_iter().map(|(_, _, id)| id).collect()
248 }
249
250 pub(crate) fn transient_ids(&self) -> Vec<OverlayId> {
251 self.entries
252 .iter()
253 .filter(|e| e.transient)
254 .map(|e| e.id)
255 .collect()
256 }
257
258 pub(crate) fn anchored_to(&self, el: crate::runtime::ElementId) -> Vec<OverlayId> {
259 if let Some(v) = self.anchor_index.get(&el) {
260 return v.clone();
261 }
262 self.entries
263 .iter()
264 .filter(|e| matches!(e.anchor, Anchor::View(a) if a == el))
265 .map(|e| e.id)
266 .collect()
267 }
268
269 pub(crate) fn outside_dismiss_ids(&self, pos: Point) -> Vec<OverlayId> {
270 self.entries
271 .iter()
272 .filter(|e| e.dismiss == Dismiss::OutsideClick && !e.rect.get().contains(pos))
273 .map(|e| e.id)
274 .collect()
275 }
276
277 pub(crate) fn escape_dismiss_id(&self) -> Option<OverlayId> {
278 self.ordered_ids()
279 .into_iter()
280 .rev()
281 .find(|id| self.get(*id).is_some_and(|e| e.dismiss == Dismiss::Escape))
282 }
283
284 pub(crate) fn focus_lost_ids(
285 &self,
286 target: Option<ElementId>,
287 inside: impl Fn(Option<ElementId>) -> bool,
288 ) -> Vec<OverlayId> {
289 self.entries
290 .iter()
291 .filter(|e| {
292 if e.dismiss != Dismiss::FocusLost {
293 return false;
294 }
295 match target {
296 None => true,
297 Some(_) => !inside(e.element.get()),
298 }
299 })
300 .map(|e| e.id)
301 .collect()
302 }
303
304 pub(crate) fn len(&self) -> usize {
305 self.entries.len()
306 }
307
308 pub(crate) fn set_offset(&mut self, id: OverlayId, offset: (f32, f32)) -> bool {
309 match self.entries.iter_mut().find(|e| e.id == id) {
310 Some(e) => {
311 e.offset = offset;
312 true
313 }
314 None => false,
315 }
316 }
317}
318
319pub(crate) fn compute_position(
320 position: Position,
321 placement: Placement,
322 anchor: Rect,
323 size: Size,
324 viewport: Rect,
325 offset: (f32, f32),
326 overflow: OverlayOverflow,
327) -> Point {
328 match position {
329 Position::Absolute => {
330 placement_position(placement, anchor, size, viewport, offset, overflow)
331 }
332 Position::Relative => clamp_to_viewport(
333 Point::new(anchor.left() + offset.0, anchor.top() + offset.1),
334 size,
335 viewport,
336 ),
337 Position::Fixed => {
338 let pos = Point::new(viewport.left() + offset.0, viewport.top() + offset.1);
339 match overflow {
340 OverlayOverflow::Visible => pos,
341 _ => clamp_to_viewport(pos, size, viewport),
342 }
343 }
344 }
345}
346
347fn clamp_to_viewport(pos: Point, size: Size, viewport: Rect) -> Point {
348 let max_x = (viewport.right() - size.width).max(viewport.left());
349 let max_y = (viewport.bottom() - size.height).max(viewport.top());
350 Point::new(
351 pos.x.clamp(viewport.left(), max_x),
352 pos.y.clamp(viewport.top(), max_y),
353 )
354}
355
356pub(crate) fn placement_position(
357 placement: Placement,
358 anchor: Rect,
359 size: Size,
360 viewport: Rect,
361 offset: (f32, f32),
362 overflow: OverlayOverflow,
363) -> Point {
364 let placement = match overflow {
365 OverlayOverflow::FlipAndClamp => flip_if_needed(placement, anchor, size, viewport),
366 _ => placement,
367 };
368 let (ox, oy) = offset;
369 let (mut x, mut y) = match placement {
370 Placement::Top => (
371 anchor.left() + (anchor.size.width - size.width) * 0.5,
372 anchor.top() - size.height,
373 ),
374 Placement::TopStart => (anchor.left(), anchor.top() - size.height),
375 Placement::TopEnd => (anchor.right() - size.width, anchor.top() - size.height),
376 Placement::Bottom => (
377 anchor.left() + (anchor.size.width - size.width) * 0.5,
378 anchor.bottom(),
379 ),
380 Placement::BottomStart => (anchor.left(), anchor.bottom()),
381 Placement::BottomEnd => (anchor.right() - size.width, anchor.bottom()),
382 Placement::Left => (
383 anchor.left() - size.width,
384 anchor.top() + (anchor.size.height - size.height) * 0.5,
385 ),
386 Placement::Right => (
387 anchor.right(),
388 anchor.top() + (anchor.size.height - size.height) * 0.5,
389 ),
390 Placement::Center => (
391 anchor.left() + (anchor.size.width - size.width) * 0.5,
392 anchor.top() + (anchor.size.height - size.height) * 0.5,
393 ),
394 Placement::Cursor => (anchor.left(), anchor.top()),
395 };
396 x += ox;
397 y += oy;
398
399 match overflow {
400 OverlayOverflow::Visible => Point::new(x, y),
401 _ => clamp_to_viewport(Point::new(x, y), size, viewport),
402 }
403}
404
405fn flip_if_needed(placement: Placement, anchor: Rect, size: Size, viewport: Rect) -> Placement {
406 match placement {
407 Placement::Bottom | Placement::BottomStart | Placement::BottomEnd => {
408 let fits_below = anchor.bottom() + size.height <= viewport.bottom();
409 let fits_above =
410 anchor.top() - size.height >= viewport.top() && size.height <= viewport.size.height;
411 if !fits_below && fits_above {
412 match placement {
413 Placement::BottomStart => Placement::TopStart,
414 Placement::BottomEnd => Placement::TopEnd,
415 _ => Placement::Top,
416 }
417 } else {
418 placement
419 }
420 }
421 Placement::Top | Placement::TopStart | Placement::TopEnd => {
422 let fits_above = anchor.top() - size.height >= viewport.top();
423 let fits_below = anchor.bottom() + size.height <= viewport.bottom()
424 && size.height <= viewport.size.height;
425 if !fits_above && fits_below {
426 match placement {
427 Placement::TopStart => Placement::BottomStart,
428 Placement::TopEnd => Placement::BottomEnd,
429 _ => Placement::Bottom,
430 }
431 } else {
432 placement
433 }
434 }
435 Placement::Right => {
436 let fits_right = anchor.right() + size.width <= viewport.right();
437 let fits_left =
438 anchor.left() - size.width >= viewport.left() && size.width <= viewport.size.width;
439 if !fits_right && fits_left {
440 Placement::Left
441 } else {
442 placement
443 }
444 }
445 Placement::Left => {
446 let fits_left = anchor.left() - size.width >= viewport.left();
447 let fits_right = anchor.right() + size.width <= viewport.right()
448 && size.width <= viewport.size.width;
449 if !fits_left && fits_right {
450 Placement::Right
451 } else {
452 placement
453 }
454 }
455 Placement::Center | Placement::Cursor => placement,
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 fn viewport() -> Rect {
464 Rect::from_xywh(0.0, 0.0, 800.0, 600.0)
465 }
466
467 #[test]
468 fn bottom_flips_to_top_when_it_does_not_fit() {
469 let anchor = Rect::from_xywh(100.0, 500.0, 120.0, 32.0);
470 let size = Size::new(200.0, 200.0);
471 let p = placement_position(
472 Placement::Bottom,
473 anchor,
474 size,
475 viewport(),
476 (0.0, 4.0),
477 OverlayOverflow::FlipAndClamp,
478 );
479 assert!(
480 p.y + size.height <= anchor.top() + 5.0,
481 "flipped above the anchor (plus 4px offset): {p:?}"
482 );
483 }
484
485 #[test]
486 fn clamp_keeps_placement_but_trims_to_viewport() {
487 let anchor = Rect::from_xywh(100.0, 500.0, 120.0, 32.0);
488 let size = Size::new(200.0, 200.0);
489 let p = placement_position(
490 Placement::Bottom,
491 anchor,
492 size,
493 viewport(),
494 (0.0, 4.0),
495 OverlayOverflow::Clamp,
496 );
497
498 assert!(
499 (p.y - 400.0).abs() < 1e-4,
500 "clamped to viewport without flip: {p:?}"
501 );
502 assert!(
503 p.y + size.height <= viewport().bottom() + 1.0,
504 "fits vertically: {p:?}"
505 );
506 }
507
508 #[test]
509 fn visible_may_bleed_outside_viewport() {
510 let anchor = Rect::from_xywh(100.0, 500.0, 120.0, 32.0);
511 let size = Size::new(200.0, 200.0);
512 let p = placement_position(
513 Placement::Bottom,
514 anchor,
515 size,
516 viewport(),
517 (0.0, 4.0),
518 OverlayOverflow::Visible,
519 );
520 assert!(
521 (p.y - (anchor.bottom() + 4.0)).abs() < 1e-4,
522 "exactly under the anchor, no fitting: {p:?}"
523 );
524 assert!(p.y + size.height > viewport().bottom(), "sticks out");
525 }
526
527 #[test]
528 fn fitting_popup_is_unaffected_by_policy() {
529 let anchor = Rect::from_xywh(100.0, 100.0, 120.0, 32.0);
530 let size = Size::new(200.0, 120.0);
531 for policy in [
532 OverlayOverflow::FlipAndClamp,
533 OverlayOverflow::Clamp,
534 OverlayOverflow::Visible,
535 ] {
536 let p = placement_position(
537 Placement::Bottom,
538 anchor,
539 size,
540 viewport(),
541 (0.0, 4.0),
542 policy,
543 );
544 assert!(
545 (p.y - (anchor.bottom() + 4.0)).abs() < 1e-4,
546 "{policy:?}: unchanged when it fits: {p:?}"
547 );
548 }
549 }
550}