1use crate::geometry::{Rect, Size};
24
25const MARGIN: f64 = 4.0;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum Align {
32 Min,
34 Center,
36 Max,
38}
39
40impl Align {
41 pub fn frac(self) -> f64 {
43 match self {
44 Align::Min => 0.0,
45 Align::Center => 0.5,
46 Align::Max => 1.0,
47 }
48 }
49
50 pub fn sign(self) -> f64 {
53 match self {
54 Align::Min => -1.0,
55 Align::Center => 0.0,
56 Align::Max => 1.0,
57 }
58 }
59
60 pub fn flip(self) -> Self {
63 match self {
64 Align::Min => Align::Max,
65 Align::Center => Align::Center,
66 Align::Max => Align::Min,
67 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub struct Align2 {
77 pub x: Align,
78 pub y: Align,
79}
80
81impl Align2 {
82 pub const LEFT_TOP: Self = Self {
83 x: Align::Min,
84 y: Align::Max,
85 };
86 pub const CENTER_TOP: Self = Self {
87 x: Align::Center,
88 y: Align::Max,
89 };
90 pub const RIGHT_TOP: Self = Self {
91 x: Align::Max,
92 y: Align::Max,
93 };
94 pub const LEFT_CENTER: Self = Self {
95 x: Align::Min,
96 y: Align::Center,
97 };
98 pub const CENTER: Self = Self {
99 x: Align::Center,
100 y: Align::Center,
101 };
102 pub const RIGHT_CENTER: Self = Self {
103 x: Align::Max,
104 y: Align::Center,
105 };
106 pub const LEFT_BOTTOM: Self = Self {
107 x: Align::Min,
108 y: Align::Min,
109 };
110 pub const CENTER_BOTTOM: Self = Self {
111 x: Align::Center,
112 y: Align::Min,
113 };
114 pub const RIGHT_BOTTOM: Self = Self {
115 x: Align::Max,
116 y: Align::Min,
117 };
118
119 pub const ALL: [(Self, &'static str); 9] = [
124 (Self::LEFT_TOP, "LEFT_TOP"),
125 (Self::LEFT_CENTER, "LEFT_CENTER"),
126 (Self::LEFT_BOTTOM, "LEFT_BOTTOM"),
127 (Self::CENTER_TOP, "CENTER_TOP"),
128 (Self::CENTER, "CENTER_CENTER"),
129 (Self::CENTER_BOTTOM, "CENTER_BOTTOM"),
130 (Self::RIGHT_TOP, "RIGHT_TOP"),
131 (Self::RIGHT_CENTER, "RIGHT_CENTER"),
132 (Self::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
133 ];
134
135 pub fn all_index(self) -> usize {
137 Self::ALL
138 .iter()
139 .position(|(a, _)| *a == self)
140 .unwrap_or(0)
141 }
142
143 pub fn point_in(self, rect: Rect) -> (f64, f64) {
145 (
146 rect.x + rect.width * self.x.frac(),
147 rect.y + rect.height * self.y.frac(),
148 )
149 }
150
151 pub fn flip_x(self) -> Self {
153 Self {
154 x: self.x.flip(),
155 y: self.y,
156 }
157 }
158
159 pub fn flip_y(self) -> Self {
161 Self {
162 x: self.x,
163 y: self.y.flip(),
164 }
165 }
166
167 pub fn flip(self) -> Self {
169 Self {
170 x: self.x.flip(),
171 y: self.y.flip(),
172 }
173 }
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct RectAlign {
180 pub parent: Align2,
182 pub child: Align2,
184}
185
186impl RectAlign {
187 pub const BOTTOM_START: Self = Self {
189 parent: Align2::LEFT_BOTTOM,
190 child: Align2::LEFT_TOP,
191 };
192 pub const BOTTOM: Self = Self {
194 parent: Align2::CENTER_BOTTOM,
195 child: Align2::CENTER_TOP,
196 };
197 pub const BOTTOM_END: Self = Self {
199 parent: Align2::RIGHT_BOTTOM,
200 child: Align2::RIGHT_TOP,
201 };
202 pub const TOP_START: Self = Self {
204 parent: Align2::LEFT_TOP,
205 child: Align2::LEFT_BOTTOM,
206 };
207 pub const TOP: Self = Self {
209 parent: Align2::CENTER_TOP,
210 child: Align2::CENTER_BOTTOM,
211 };
212 pub const TOP_END: Self = Self {
214 parent: Align2::RIGHT_TOP,
215 child: Align2::RIGHT_BOTTOM,
216 };
217 pub const RIGHT_START: Self = Self {
219 parent: Align2::RIGHT_TOP,
220 child: Align2::LEFT_TOP,
221 };
222 pub const RIGHT: Self = Self {
224 parent: Align2::RIGHT_CENTER,
225 child: Align2::LEFT_CENTER,
226 };
227 pub const RIGHT_END: Self = Self {
229 parent: Align2::RIGHT_BOTTOM,
230 child: Align2::LEFT_BOTTOM,
231 };
232 pub const LEFT_START: Self = Self {
234 parent: Align2::LEFT_TOP,
235 child: Align2::RIGHT_TOP,
236 };
237 pub const LEFT: Self = Self {
239 parent: Align2::LEFT_CENTER,
240 child: Align2::RIGHT_CENTER,
241 };
242 pub const LEFT_END: Self = Self {
244 parent: Align2::LEFT_BOTTOM,
245 child: Align2::RIGHT_BOTTOM,
246 };
247
248 pub const PRESETS: [(Self, &'static str); 12] = [
251 (Self::TOP_START, "TOP_START"),
252 (Self::TOP, "TOP"),
253 (Self::TOP_END, "TOP_END"),
254 (Self::RIGHT_START, "RIGHT_START"),
255 (Self::RIGHT, "RIGHT"),
256 (Self::RIGHT_END, "RIGHT_END"),
257 (Self::BOTTOM_START, "BOTTOM_START"),
258 (Self::BOTTOM, "BOTTOM"),
259 (Self::BOTTOM_END, "BOTTOM_END"),
260 (Self::LEFT_START, "LEFT_START"),
261 (Self::LEFT, "LEFT"),
262 (Self::LEFT_END, "LEFT_END"),
263 ];
264
265 pub const MENU_ALIGNS: [Self; 12] = [
269 Self::BOTTOM_START,
270 Self::BOTTOM_END,
271 Self::TOP_START,
272 Self::TOP_END,
273 Self::RIGHT_END,
274 Self::RIGHT_START,
275 Self::LEFT_END,
276 Self::LEFT_START,
277 Self::TOP,
279 Self::RIGHT,
280 Self::BOTTOM,
281 Self::LEFT,
282 ];
283
284 pub fn preset_label(self) -> Option<&'static str> {
287 Self::PRESETS
288 .iter()
289 .find(|(a, _)| *a == self)
290 .map(|(_, label)| *label)
291 }
292
293 pub fn flip_x(self) -> Self {
295 Self {
296 parent: self.parent.flip_x(),
297 child: self.child.flip_x(),
298 }
299 }
300
301 pub fn flip_y(self) -> Self {
303 Self {
304 parent: self.parent.flip_y(),
305 child: self.child.flip_y(),
306 }
307 }
308
309 pub fn flip(self) -> Self {
311 Self {
312 parent: self.parent.flip(),
313 child: self.child.flip(),
314 }
315 }
316
317 pub fn symmetries(self) -> [Self; 3] {
320 [self.flip_x(), self.flip_y(), self.flip()]
321 }
322
323 pub fn find_best_align(
330 values_to_try: impl Iterator<Item = Self>,
331 content_rect: Rect,
332 parent_rect: Rect,
333 gap: f64,
334 expected_size: Size,
335 ) -> Option<Self> {
336 let mut first_choice = None;
337 for align in values_to_try {
338 first_choice = first_choice.or(Some(align));
339 let suggested = align.place_child(parent_rect, expected_size, gap);
340 if contains_rect(content_rect, suggested) {
341 return Some(align);
342 }
343 }
344 first_choice
345 }
346
347 pub fn place_child(self, parent: Rect, size: Size, gap: f64) -> Rect {
355 let (px, py) = self.parent.point_in(parent);
356 let gx = if self.parent.x != self.child.x {
357 self.parent.x.sign()
358 } else {
359 0.0
360 };
361 let gy = if self.parent.y != self.child.y {
362 self.parent.y.sign()
363 } else {
364 0.0
365 };
366 let ax = px + gap * gx;
367 let ay = py + gap * gy;
368 let cx = ax - size.width * self.child.x.frac();
369 let cy = ay - size.height * self.child.y.frac();
370 Rect::new(cx, cy, size.width, size.height)
371 }
372
373 pub fn place_child_clamped(self, parent: Rect, size: Size, gap: f64, viewport: Size) -> Rect {
377 clamp_rect(self.place_child(parent, size, gap), viewport)
378 }
379}
380
381fn contains_rect(outer: Rect, inner: Rect) -> bool {
383 inner.x >= outer.x
384 && inner.y >= outer.y
385 && inner.x + inner.width <= outer.x + outer.width
386 && inner.y + inner.height <= outer.y + outer.height
387}
388
389pub fn clamp_rect(rect: Rect, viewport: Size) -> Rect {
393 let max_x = (viewport.width - rect.width - MARGIN).max(MARGIN);
394 let max_y = (viewport.height - rect.height - MARGIN).max(MARGIN);
395 Rect::new(
396 rect.x.clamp(MARGIN, max_x),
397 rect.y.clamp(MARGIN, max_y),
398 rect.width,
399 rect.height,
400 )
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 const PARENT: Rect = Rect {
408 x: 100.0,
409 y: 100.0,
410 width: 40.0,
411 height: 20.0,
412 };
413 const SIZE: Size = Size {
414 width: 60.0,
415 height: 30.0,
416 };
417 const GAP: f64 = 4.0;
418
419 #[test]
420 fn bottom_start_hangs_below_left_aligned() {
421 let r = RectAlign::BOTTOM_START.place_child(PARENT, SIZE, GAP);
424 assert_eq!(r, Rect::new(100.0, 66.0, 60.0, 30.0));
425 assert_eq!(r.x, PARENT.x);
427 assert_eq!(r.y + r.height, PARENT.y - GAP);
429 }
430
431 #[test]
432 fn top_is_centered_above() {
433 let r = RectAlign::TOP.place_child(PARENT, SIZE, GAP);
434 assert_eq!(r.y, PARENT.y + PARENT.height + GAP);
436 let popup_center_x = r.x + r.width * 0.5;
438 let parent_center_x = PARENT.x + PARENT.width * 0.5;
439 assert!((popup_center_x - parent_center_x).abs() < 1e-9);
440 }
441
442 #[test]
443 fn right_is_centered_to_the_right() {
444 let r = RectAlign::RIGHT.place_child(PARENT, SIZE, GAP);
445 assert_eq!(r.x, PARENT.x + PARENT.width + GAP);
447 let popup_center_y = r.y + r.height * 0.5;
449 let parent_center_y = PARENT.y + PARENT.height * 0.5;
450 assert!((popup_center_y - parent_center_y).abs() < 1e-9);
451 }
452
453 #[test]
454 fn left_is_centered_to_the_left() {
455 let r = RectAlign::LEFT.place_child(PARENT, SIZE, GAP);
456 assert_eq!(r.x + r.width, PARENT.x - GAP);
458 let popup_center_y = r.y + r.height * 0.5;
459 let parent_center_y = PARENT.y + PARENT.height * 0.5;
460 assert!((popup_center_y - parent_center_y).abs() < 1e-9);
461 }
462
463 #[test]
464 fn zero_gap_puts_child_anchor_on_parent_anchor() {
465 let align = RectAlign {
468 parent: Align2::RIGHT_TOP,
469 child: Align2::LEFT_BOTTOM,
470 };
471 let r = align.place_child(PARENT, SIZE, 0.0);
472 let (px, py) = Align2::RIGHT_TOP.point_in(PARENT);
473 let (cx, cy) = Align2::LEFT_BOTTOM.point_in(r);
474 assert!((px - cx).abs() < 1e-9);
475 assert!((py - cy).abs() < 1e-9);
476 }
477
478 #[test]
479 fn clamp_keeps_popup_on_screen() {
480 let parent = Rect::new(2.0, 6.0, 40.0, 20.0);
482 let viewport = Size::new(400.0, 300.0);
483 let r = RectAlign::BOTTOM_START.place_child_clamped(parent, SIZE, GAP, viewport);
484 assert!(r.x >= 4.0);
485 assert!(r.y >= 4.0);
486 assert!(r.x + r.width <= viewport.width);
487 assert!(r.y + r.height <= viewport.height);
488 }
489
490 #[test]
491 fn presets_and_anchor_tables_round_trip() {
492 assert_eq!(RectAlign::BOTTOM.preset_label(), Some("BOTTOM"));
495 for (i, (a, _)) in Align2::ALL.iter().enumerate() {
496 assert_eq!(a.all_index(), i);
497 }
498 }
499
500 #[test]
501 fn flips_mirror_the_named_presets() {
502 assert_eq!(RectAlign::BOTTOM_START.flip_y(), RectAlign::TOP_START);
504 assert_eq!(RectAlign::TOP_END.flip_y(), RectAlign::BOTTOM_END);
505 assert_eq!(RectAlign::LEFT.flip_x(), RectAlign::RIGHT);
506 assert_eq!(RectAlign::RIGHT_START.flip_x(), RectAlign::LEFT_START);
507 assert_eq!(RectAlign::BOTTOM_START.flip(), RectAlign::TOP_END);
508 assert_eq!(
510 RectAlign::BOTTOM_START.symmetries(),
511 [
512 RectAlign::BOTTOM_END,
513 RectAlign::TOP_START,
514 RectAlign::TOP_END
515 ]
516 );
517 }
518
519 fn candidates(align: RectAlign) -> impl Iterator<Item = RectAlign> {
522 std::iter::once(align)
523 .chain(align.symmetries())
524 .chain(RectAlign::MENU_ALIGNS)
525 }
526
527 #[test]
528 fn find_best_align_flips_at_each_screen_edge() {
529 let screen = Rect::new(0.0, 0.0, 400.0, 300.0);
530 let size = Size::new(100.0, 80.0);
531
532 let parent = Rect::new(150.0, 10.0, 40.0, 20.0);
535 let best = RectAlign::find_best_align(
536 candidates(RectAlign::BOTTOM_START),
537 screen,
538 parent,
539 GAP,
540 size,
541 );
542 assert_eq!(best, Some(RectAlign::TOP_START));
543
544 let parent = Rect::new(150.0, 270.0, 40.0, 20.0);
546 let best = RectAlign::find_best_align(
547 candidates(RectAlign::TOP_START),
548 screen,
549 parent,
550 GAP,
551 size,
552 );
553 assert_eq!(best, Some(RectAlign::BOTTOM_START));
554
555 let parent = Rect::new(340.0, 100.0, 40.0, 20.0);
557 let best = RectAlign::find_best_align(
558 candidates(RectAlign::RIGHT_START),
559 screen,
560 parent,
561 GAP,
562 size,
563 );
564 assert_eq!(best, Some(RectAlign::LEFT_START));
565
566 let parent = Rect::new(10.0, 100.0, 40.0, 20.0);
568 let best = RectAlign::find_best_align(
569 candidates(RectAlign::LEFT_START),
570 screen,
571 parent,
572 GAP,
573 size,
574 );
575 assert_eq!(best, Some(RectAlign::RIGHT_START));
576 }
577
578 #[test]
579 fn find_best_align_keeps_preferred_align_when_it_fits() {
580 let screen = Rect::new(0.0, 0.0, 400.0, 300.0);
581 let parent = Rect::new(150.0, 150.0, 40.0, 20.0);
582 let best = RectAlign::find_best_align(
583 candidates(RectAlign::BOTTOM_START),
584 screen,
585 parent,
586 GAP,
587 Size::new(100.0, 80.0),
588 );
589 assert_eq!(best, Some(RectAlign::BOTTOM_START));
590 }
591
592 #[test]
593 fn find_best_align_falls_back_to_first_candidate_when_nothing_fits() {
594 let screen = Rect::new(0.0, 0.0, 100.0, 100.0);
597 let parent = Rect::new(40.0, 40.0, 20.0, 10.0);
598 let big = Size::new(500.0, 500.0);
599 let best = RectAlign::find_best_align(
600 candidates(RectAlign::BOTTOM_START),
601 screen,
602 parent,
603 GAP,
604 big,
605 );
606 assert_eq!(best, Some(RectAlign::BOTTOM_START));
607 assert_eq!(
608 RectAlign::find_best_align(std::iter::empty(), screen, parent, GAP, big),
609 None
610 );
611 }
612}