1#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
3pub enum OverlayPlacement {
4 Auto,
6 #[default]
8 Top,
9 Bottom,
11 Start,
13 End,
15}
16
17impl OverlayPlacement {
18 pub const DEFAULT_FALLBACKS: [OverlayPlacement; 4] = [
20 OverlayPlacement::Top,
21 OverlayPlacement::End,
22 OverlayPlacement::Bottom,
23 OverlayPlacement::Start,
24 ];
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
29pub struct OverlayRect {
30 pub x: f64,
31 pub y: f64,
32 pub width: f64,
33 pub height: f64,
34}
35
36impl OverlayRect {
37 pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
38 Self {
39 x,
40 y,
41 width,
42 height,
43 }
44 }
45
46 pub fn right(self) -> f64 {
47 self.x + self.width
48 }
49
50 pub fn bottom(self) -> f64 {
51 self.y + self.height
52 }
53
54 pub fn center_x(self) -> f64 {
55 self.x + self.width / 2.0
56 }
57
58 pub fn center_y(self) -> f64 {
59 self.y + self.height / 2.0
60 }
61}
62
63#[derive(Clone, Copy, Debug, Default, PartialEq)]
65pub struct OverlayOffset {
66 pub skidding: f64,
68 pub distance: f64,
70}
71
72impl OverlayOffset {
73 pub const ZERO: Self = Self {
74 skidding: 0.0,
75 distance: 0.0,
76 };
77
78 pub const TOOLTIP: Self = Self {
80 skidding: 0.0,
81 distance: 6.0,
82 };
83
84 pub const POPOVER: Self = Self {
86 skidding: 0.0,
87 distance: 8.0,
88 };
89}
90
91const ARROW_SIZE: f64 = 16.0;
93const ARROW_EDGE_INSET: f64 = 8.0;
96
97#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct OverlayPosition {
100 pub x: f64,
101 pub y: f64,
102 pub placement: OverlayPlacement,
103 pub fits: bool,
105 pub arrow: f64,
110}
111
112impl OverlayPosition {
113 pub fn rect(self, overlay_size: OverlayRect) -> OverlayRect {
114 OverlayRect::new(self.x, self.y, overlay_size.width, overlay_size.height)
115 }
116}
117
118fn arrow_offset(trigger: OverlayRect, rect: OverlayRect, placement: OverlayPlacement) -> f64 {
121 let (target, cross_size) = match placement {
122 OverlayPlacement::Start | OverlayPlacement::End => {
123 (trigger.center_y() - rect.y, rect.height)
124 }
125 _ => (trigger.center_x() - rect.x, rect.width),
127 };
128 let lo = ARROW_EDGE_INSET + ARROW_SIZE / 2.0;
129 let hi = (cross_size - ARROW_EDGE_INSET - ARROW_SIZE / 2.0).max(lo);
130 target.clamp(lo, hi)
131}
132
133pub fn calculate_overlay_position(
139 trigger: OverlayRect,
140 overlay_size: OverlayRect,
141 boundary: OverlayRect,
142 requested: OverlayPlacement,
143 fallback_placements: &[OverlayPlacement],
144 offset: OverlayOffset,
145 boundary_padding: f64,
146) -> OverlayPosition {
147 let candidates = candidate_placements(requested, fallback_placements);
148 let mut best: Option<(OverlayPlacement, OverlayRect, f64)> = None;
149
150 for placement in candidates {
151 let rect = placed_rect(trigger, overlay_size, placement, offset);
152 if fits_boundary(rect, boundary, boundary_padding) {
153 return OverlayPosition {
154 x: rect.x,
155 y: rect.y,
156 placement,
157 fits: true,
158 arrow: arrow_offset(trigger, rect, placement),
159 };
160 }
161
162 let visible = visible_area(rect, boundary, boundary_padding);
163 if best
164 .map(|(_, _, best_visible)| visible > best_visible)
165 .unwrap_or(true)
166 {
167 best = Some((placement, rect, visible));
168 }
169 }
170
171 let (placement, rect, _) = best.unwrap_or_else(|| {
172 let placement = OverlayPlacement::Top;
173 (
174 placement,
175 placed_rect(trigger, overlay_size, placement, offset),
176 0.0,
177 )
178 });
179 let clamped = clamp_to_boundary(rect, boundary, boundary_padding);
180
181 OverlayPosition {
182 x: clamped.x,
183 y: clamped.y,
184 placement,
185 fits: false,
186 arrow: arrow_offset(trigger, clamped, placement),
187 }
188}
189
190fn candidate_placements(
191 requested: OverlayPlacement,
192 fallback_placements: &[OverlayPlacement],
193) -> Vec<OverlayPlacement> {
194 let mut candidates = Vec::new();
195
196 if requested == OverlayPlacement::Auto {
197 push_candidates(&mut candidates, fallback_placements);
198 if candidates.is_empty() {
199 push_candidates(&mut candidates, &OverlayPlacement::DEFAULT_FALLBACKS);
200 }
201 } else {
202 candidates.push(requested);
203 push_candidates(&mut candidates, fallback_placements);
204 }
205
206 candidates
207}
208
209fn push_candidates(candidates: &mut Vec<OverlayPlacement>, placements: &[OverlayPlacement]) {
210 for placement in placements {
211 if *placement != OverlayPlacement::Auto && !candidates.contains(placement) {
212 candidates.push(*placement);
213 }
214 }
215}
216
217fn placed_rect(
218 trigger: OverlayRect,
219 overlay_size: OverlayRect,
220 placement: OverlayPlacement,
221 offset: OverlayOffset,
222) -> OverlayRect {
223 match placement {
224 OverlayPlacement::Auto => placed_rect(trigger, overlay_size, OverlayPlacement::Top, offset),
225 OverlayPlacement::Top => OverlayRect::new(
226 trigger.center_x() - overlay_size.width / 2.0 + offset.skidding,
227 trigger.y - overlay_size.height - offset.distance,
228 overlay_size.width,
229 overlay_size.height,
230 ),
231 OverlayPlacement::Bottom => OverlayRect::new(
232 trigger.center_x() - overlay_size.width / 2.0 + offset.skidding,
233 trigger.bottom() + offset.distance,
234 overlay_size.width,
235 overlay_size.height,
236 ),
237 OverlayPlacement::Start => OverlayRect::new(
238 trigger.x - overlay_size.width - offset.distance,
239 trigger.center_y() - overlay_size.height / 2.0 + offset.skidding,
240 overlay_size.width,
241 overlay_size.height,
242 ),
243 OverlayPlacement::End => OverlayRect::new(
244 trigger.right() + offset.distance,
245 trigger.center_y() - overlay_size.height / 2.0 + offset.skidding,
246 overlay_size.width,
247 overlay_size.height,
248 ),
249 }
250}
251
252fn fits_boundary(rect: OverlayRect, boundary: OverlayRect, padding: f64) -> bool {
253 rect.x >= boundary.x + padding
254 && rect.y >= boundary.y + padding
255 && rect.right() <= boundary.right() - padding
256 && rect.bottom() <= boundary.bottom() - padding
257}
258
259fn visible_area(rect: OverlayRect, boundary: OverlayRect, padding: f64) -> f64 {
260 let min_x = boundary.x + padding;
261 let min_y = boundary.y + padding;
262 let max_x = boundary.right() - padding;
263 let max_y = boundary.bottom() - padding;
264
265 let width = (rect.right().min(max_x) - rect.x.max(min_x)).max(0.0);
266 let height = (rect.bottom().min(max_y) - rect.y.max(min_y)).max(0.0);
267 width * height
268}
269
270fn clamp_to_boundary(rect: OverlayRect, boundary: OverlayRect, padding: f64) -> OverlayRect {
271 let min_x = boundary.x + padding;
272 let min_y = boundary.y + padding;
273 let max_x = (boundary.right() - padding - rect.width).max(min_x);
274 let max_y = (boundary.bottom() - padding - rect.height).max(min_y);
275
276 OverlayRect::new(
277 rect.x.clamp(min_x, max_x),
278 rect.y.clamp(min_y, max_y),
279 rect.width,
280 rect.height,
281 )
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 fn trigger() -> OverlayRect {
289 OverlayRect::new(100.0, 100.0, 40.0, 20.0)
290 }
291
292 fn overlay() -> OverlayRect {
293 OverlayRect::new(0.0, 0.0, 80.0, 30.0)
294 }
295
296 fn boundary() -> OverlayRect {
297 OverlayRect::new(0.0, 0.0, 300.0, 300.0)
298 }
299
300 #[test]
301 fn requested_top_fits() {
302 let position = calculate_overlay_position(
303 trigger(),
304 overlay(),
305 boundary(),
306 OverlayPlacement::Top,
307 &OverlayPlacement::DEFAULT_FALLBACKS,
308 OverlayOffset::TOOLTIP,
309 0.0,
310 );
311
312 assert_eq!(position.placement, OverlayPlacement::Top);
313 assert!(position.fits);
314 assert_eq!(position.x, 80.0);
315 assert_eq!(position.y, 64.0);
316 }
317
318 #[test]
319 fn offset_skids_on_cross_axis() {
320 let position = calculate_overlay_position(
321 trigger(),
322 overlay(),
323 boundary(),
324 OverlayPlacement::Bottom,
325 &[],
326 OverlayOffset {
327 skidding: 10.0,
328 distance: 12.0,
329 },
330 0.0,
331 );
332
333 assert_eq!(position.placement, OverlayPlacement::Bottom);
334 assert!(position.fits);
335 assert_eq!(position.x, 90.0);
336 assert_eq!(position.y, 132.0);
337 }
338
339 #[test]
340 fn falls_back_when_requested_placement_overflows() {
341 let edge_trigger = OverlayRect::new(100.0, 10.0, 40.0, 20.0);
342 let position = calculate_overlay_position(
343 edge_trigger,
344 overlay(),
345 boundary(),
346 OverlayPlacement::Top,
347 &[OverlayPlacement::Bottom, OverlayPlacement::End],
348 OverlayOffset::TOOLTIP,
349 0.0,
350 );
351
352 assert_eq!(position.placement, OverlayPlacement::Bottom);
353 assert!(position.fits);
354 assert_eq!(position.y, 36.0);
355 }
356
357 #[test]
358 fn auto_uses_first_fitting_fallback() {
359 let edge_trigger = OverlayRect::new(100.0, 10.0, 40.0, 20.0);
360 let position = calculate_overlay_position(
361 edge_trigger,
362 overlay(),
363 boundary(),
364 OverlayPlacement::Auto,
365 &[
366 OverlayPlacement::Top,
367 OverlayPlacement::Bottom,
368 OverlayPlacement::End,
369 ],
370 OverlayOffset::TOOLTIP,
371 0.0,
372 );
373
374 assert_eq!(position.placement, OverlayPlacement::Bottom);
375 assert!(position.fits);
376 }
377
378 #[test]
379 fn start_and_end_place_on_inline_axis() {
380 let start = calculate_overlay_position(
381 trigger(),
382 overlay(),
383 boundary(),
384 OverlayPlacement::Start,
385 &[],
386 OverlayOffset::POPOVER,
387 0.0,
388 );
389 let end = calculate_overlay_position(
390 trigger(),
391 overlay(),
392 boundary(),
393 OverlayPlacement::End,
394 &[],
395 OverlayOffset::POPOVER,
396 0.0,
397 );
398
399 assert_eq!(start.x, 12.0);
400 assert_eq!(start.y, 95.0);
401 assert_eq!(end.x, 148.0);
402 assert_eq!(end.y, 95.0);
403 }
404
405 #[test]
406 fn clamps_best_candidate_to_boundary_padding() {
407 let edge_trigger = OverlayRect::new(0.0, 120.0, 20.0, 20.0);
408 let position = calculate_overlay_position(
409 edge_trigger,
410 overlay(),
411 boundary(),
412 OverlayPlacement::Top,
413 &[],
414 OverlayOffset::ZERO,
415 8.0,
416 );
417
418 assert_eq!(position.placement, OverlayPlacement::Top);
419 assert!(!position.fits);
420 assert_eq!(position.x, 8.0);
421 assert_eq!(position.y, 90.0);
422 }
423
424 #[test]
425 fn arrow_is_centred_when_overlay_fits() {
426 let position = calculate_overlay_position(
428 OverlayRect::new(130.0, 20.0, 40.0, 20.0), OverlayRect::new(0.0, 0.0, 80.0, 30.0),
430 boundary(),
431 OverlayPlacement::Bottom,
432 &[],
433 OverlayOffset::ZERO,
434 0.0,
435 );
436 assert!(position.fits);
437 assert_eq!(position.x, 110.0);
438 assert_eq!(position.arrow, 40.0);
440 }
441
442 #[test]
443 fn arrow_tracks_trigger_after_horizontal_clamp() {
444 let position = calculate_overlay_position(
448 OverlayRect::new(280.0, 20.0, 20.0, 20.0), OverlayRect::new(0.0, 0.0, 120.0, 60.0),
450 boundary(), OverlayPlacement::Bottom,
452 &[],
453 OverlayOffset::ZERO,
454 0.0,
455 );
456 assert_eq!(position.placement, OverlayPlacement::Bottom);
457 assert!(!position.fits);
458 assert_eq!(position.x, 180.0); assert_eq!(position.arrow, 104.0);
462 }
463
464 #[test]
465 fn clamps_oversized_overlay_to_boundary_start() {
466 let oversized = OverlayRect::new(0.0, 0.0, 400.0, 400.0);
467 let position = calculate_overlay_position(
468 trigger(),
469 oversized,
470 boundary(),
471 OverlayPlacement::Bottom,
472 &[],
473 OverlayOffset::ZERO,
474 8.0,
475 );
476
477 assert!(!position.fits);
478 assert_eq!(position.x, 8.0);
479 assert_eq!(position.y, 8.0);
480 }
481}