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
91#[derive(Clone, Copy, Debug, PartialEq)]
93pub struct OverlayPosition {
94 pub x: f64,
95 pub y: f64,
96 pub placement: OverlayPlacement,
97 pub fits: bool,
99}
100
101impl OverlayPosition {
102 pub fn rect(self, overlay_size: OverlayRect) -> OverlayRect {
103 OverlayRect::new(self.x, self.y, overlay_size.width, overlay_size.height)
104 }
105}
106
107pub fn calculate_overlay_position(
113 trigger: OverlayRect,
114 overlay_size: OverlayRect,
115 boundary: OverlayRect,
116 requested: OverlayPlacement,
117 fallback_placements: &[OverlayPlacement],
118 offset: OverlayOffset,
119 boundary_padding: f64,
120) -> OverlayPosition {
121 let candidates = candidate_placements(requested, fallback_placements);
122 let mut best: Option<(OverlayPlacement, OverlayRect, f64)> = None;
123
124 for placement in candidates {
125 let rect = placed_rect(trigger, overlay_size, placement, offset);
126 if fits_boundary(rect, boundary, boundary_padding) {
127 return OverlayPosition {
128 x: rect.x,
129 y: rect.y,
130 placement,
131 fits: true,
132 };
133 }
134
135 let visible = visible_area(rect, boundary, boundary_padding);
136 if best
137 .map(|(_, _, best_visible)| visible > best_visible)
138 .unwrap_or(true)
139 {
140 best = Some((placement, rect, visible));
141 }
142 }
143
144 let (placement, rect, _) = best.unwrap_or_else(|| {
145 let placement = OverlayPlacement::Top;
146 (
147 placement,
148 placed_rect(trigger, overlay_size, placement, offset),
149 0.0,
150 )
151 });
152 let clamped = clamp_to_boundary(rect, boundary, boundary_padding);
153
154 OverlayPosition {
155 x: clamped.x,
156 y: clamped.y,
157 placement,
158 fits: false,
159 }
160}
161
162fn candidate_placements(
163 requested: OverlayPlacement,
164 fallback_placements: &[OverlayPlacement],
165) -> Vec<OverlayPlacement> {
166 let mut candidates = Vec::new();
167
168 if requested == OverlayPlacement::Auto {
169 push_candidates(&mut candidates, fallback_placements);
170 if candidates.is_empty() {
171 push_candidates(&mut candidates, &OverlayPlacement::DEFAULT_FALLBACKS);
172 }
173 } else {
174 candidates.push(requested);
175 push_candidates(&mut candidates, fallback_placements);
176 }
177
178 candidates
179}
180
181fn push_candidates(candidates: &mut Vec<OverlayPlacement>, placements: &[OverlayPlacement]) {
182 for placement in placements {
183 if *placement != OverlayPlacement::Auto && !candidates.contains(placement) {
184 candidates.push(*placement);
185 }
186 }
187}
188
189fn placed_rect(
190 trigger: OverlayRect,
191 overlay_size: OverlayRect,
192 placement: OverlayPlacement,
193 offset: OverlayOffset,
194) -> OverlayRect {
195 match placement {
196 OverlayPlacement::Auto => placed_rect(trigger, overlay_size, OverlayPlacement::Top, offset),
197 OverlayPlacement::Top => OverlayRect::new(
198 trigger.center_x() - overlay_size.width / 2.0 + offset.skidding,
199 trigger.y - overlay_size.height - offset.distance,
200 overlay_size.width,
201 overlay_size.height,
202 ),
203 OverlayPlacement::Bottom => OverlayRect::new(
204 trigger.center_x() - overlay_size.width / 2.0 + offset.skidding,
205 trigger.bottom() + offset.distance,
206 overlay_size.width,
207 overlay_size.height,
208 ),
209 OverlayPlacement::Start => OverlayRect::new(
210 trigger.x - overlay_size.width - offset.distance,
211 trigger.center_y() - overlay_size.height / 2.0 + offset.skidding,
212 overlay_size.width,
213 overlay_size.height,
214 ),
215 OverlayPlacement::End => OverlayRect::new(
216 trigger.right() + offset.distance,
217 trigger.center_y() - overlay_size.height / 2.0 + offset.skidding,
218 overlay_size.width,
219 overlay_size.height,
220 ),
221 }
222}
223
224fn fits_boundary(rect: OverlayRect, boundary: OverlayRect, padding: f64) -> bool {
225 rect.x >= boundary.x + padding
226 && rect.y >= boundary.y + padding
227 && rect.right() <= boundary.right() - padding
228 && rect.bottom() <= boundary.bottom() - padding
229}
230
231fn visible_area(rect: OverlayRect, boundary: OverlayRect, padding: f64) -> f64 {
232 let min_x = boundary.x + padding;
233 let min_y = boundary.y + padding;
234 let max_x = boundary.right() - padding;
235 let max_y = boundary.bottom() - padding;
236
237 let width = (rect.right().min(max_x) - rect.x.max(min_x)).max(0.0);
238 let height = (rect.bottom().min(max_y) - rect.y.max(min_y)).max(0.0);
239 width * height
240}
241
242fn clamp_to_boundary(rect: OverlayRect, boundary: OverlayRect, padding: f64) -> OverlayRect {
243 let min_x = boundary.x + padding;
244 let min_y = boundary.y + padding;
245 let max_x = (boundary.right() - padding - rect.width).max(min_x);
246 let max_y = (boundary.bottom() - padding - rect.height).max(min_y);
247
248 OverlayRect::new(
249 rect.x.clamp(min_x, max_x),
250 rect.y.clamp(min_y, max_y),
251 rect.width,
252 rect.height,
253 )
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn trigger() -> OverlayRect {
261 OverlayRect::new(100.0, 100.0, 40.0, 20.0)
262 }
263
264 fn overlay() -> OverlayRect {
265 OverlayRect::new(0.0, 0.0, 80.0, 30.0)
266 }
267
268 fn boundary() -> OverlayRect {
269 OverlayRect::new(0.0, 0.0, 300.0, 300.0)
270 }
271
272 #[test]
273 fn requested_top_fits() {
274 let position = calculate_overlay_position(
275 trigger(),
276 overlay(),
277 boundary(),
278 OverlayPlacement::Top,
279 &OverlayPlacement::DEFAULT_FALLBACKS,
280 OverlayOffset::TOOLTIP,
281 0.0,
282 );
283
284 assert_eq!(position.placement, OverlayPlacement::Top);
285 assert!(position.fits);
286 assert_eq!(position.x, 80.0);
287 assert_eq!(position.y, 64.0);
288 }
289
290 #[test]
291 fn offset_skids_on_cross_axis() {
292 let position = calculate_overlay_position(
293 trigger(),
294 overlay(),
295 boundary(),
296 OverlayPlacement::Bottom,
297 &[],
298 OverlayOffset {
299 skidding: 10.0,
300 distance: 12.0,
301 },
302 0.0,
303 );
304
305 assert_eq!(position.placement, OverlayPlacement::Bottom);
306 assert!(position.fits);
307 assert_eq!(position.x, 90.0);
308 assert_eq!(position.y, 132.0);
309 }
310
311 #[test]
312 fn falls_back_when_requested_placement_overflows() {
313 let edge_trigger = OverlayRect::new(100.0, 10.0, 40.0, 20.0);
314 let position = calculate_overlay_position(
315 edge_trigger,
316 overlay(),
317 boundary(),
318 OverlayPlacement::Top,
319 &[OverlayPlacement::Bottom, OverlayPlacement::End],
320 OverlayOffset::TOOLTIP,
321 0.0,
322 );
323
324 assert_eq!(position.placement, OverlayPlacement::Bottom);
325 assert!(position.fits);
326 assert_eq!(position.y, 36.0);
327 }
328
329 #[test]
330 fn auto_uses_first_fitting_fallback() {
331 let edge_trigger = OverlayRect::new(100.0, 10.0, 40.0, 20.0);
332 let position = calculate_overlay_position(
333 edge_trigger,
334 overlay(),
335 boundary(),
336 OverlayPlacement::Auto,
337 &[
338 OverlayPlacement::Top,
339 OverlayPlacement::Bottom,
340 OverlayPlacement::End,
341 ],
342 OverlayOffset::TOOLTIP,
343 0.0,
344 );
345
346 assert_eq!(position.placement, OverlayPlacement::Bottom);
347 assert!(position.fits);
348 }
349
350 #[test]
351 fn start_and_end_place_on_inline_axis() {
352 let start = calculate_overlay_position(
353 trigger(),
354 overlay(),
355 boundary(),
356 OverlayPlacement::Start,
357 &[],
358 OverlayOffset::POPOVER,
359 0.0,
360 );
361 let end = calculate_overlay_position(
362 trigger(),
363 overlay(),
364 boundary(),
365 OverlayPlacement::End,
366 &[],
367 OverlayOffset::POPOVER,
368 0.0,
369 );
370
371 assert_eq!(start.x, 12.0);
372 assert_eq!(start.y, 95.0);
373 assert_eq!(end.x, 148.0);
374 assert_eq!(end.y, 95.0);
375 }
376
377 #[test]
378 fn clamps_best_candidate_to_boundary_padding() {
379 let edge_trigger = OverlayRect::new(0.0, 120.0, 20.0, 20.0);
380 let position = calculate_overlay_position(
381 edge_trigger,
382 overlay(),
383 boundary(),
384 OverlayPlacement::Top,
385 &[],
386 OverlayOffset::ZERO,
387 8.0,
388 );
389
390 assert_eq!(position.placement, OverlayPlacement::Top);
391 assert!(!position.fits);
392 assert_eq!(position.x, 8.0);
393 assert_eq!(position.y, 90.0);
394 }
395
396 #[test]
397 fn clamps_oversized_overlay_to_boundary_start() {
398 let oversized = OverlayRect::new(0.0, 0.0, 400.0, 400.0);
399 let position = calculate_overlay_position(
400 trigger(),
401 oversized,
402 boundary(),
403 OverlayPlacement::Bottom,
404 &[],
405 OverlayOffset::ZERO,
406 8.0,
407 );
408
409 assert!(!position.fits);
410 assert_eq!(position.x, 8.0);
411 assert_eq!(position.y, 8.0);
412 }
413}