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.iter().position(|(a, _)| *a == self).unwrap_or(0)
138 }
139
140 pub fn point_in(self, rect: Rect) -> (f64, f64) {
142 (
143 rect.x + rect.width * self.x.frac(),
144 rect.y + rect.height * self.y.frac(),
145 )
146 }
147
148 pub fn flip_x(self) -> Self {
150 Self {
151 x: self.x.flip(),
152 y: self.y,
153 }
154 }
155
156 pub fn flip_y(self) -> Self {
158 Self {
159 x: self.x,
160 y: self.y.flip(),
161 }
162 }
163
164 pub fn flip(self) -> Self {
166 Self {
167 x: self.x.flip(),
168 y: self.y.flip(),
169 }
170 }
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
176pub struct RectAlign {
177 pub parent: Align2,
179 pub child: Align2,
181}
182
183impl RectAlign {
184 pub const BOTTOM_START: Self = Self {
186 parent: Align2::LEFT_BOTTOM,
187 child: Align2::LEFT_TOP,
188 };
189 pub const BOTTOM: Self = Self {
191 parent: Align2::CENTER_BOTTOM,
192 child: Align2::CENTER_TOP,
193 };
194 pub const BOTTOM_END: Self = Self {
196 parent: Align2::RIGHT_BOTTOM,
197 child: Align2::RIGHT_TOP,
198 };
199 pub const TOP_START: Self = Self {
201 parent: Align2::LEFT_TOP,
202 child: Align2::LEFT_BOTTOM,
203 };
204 pub const TOP: Self = Self {
206 parent: Align2::CENTER_TOP,
207 child: Align2::CENTER_BOTTOM,
208 };
209 pub const TOP_END: Self = Self {
211 parent: Align2::RIGHT_TOP,
212 child: Align2::RIGHT_BOTTOM,
213 };
214 pub const RIGHT_START: Self = Self {
216 parent: Align2::RIGHT_TOP,
217 child: Align2::LEFT_TOP,
218 };
219 pub const RIGHT: Self = Self {
221 parent: Align2::RIGHT_CENTER,
222 child: Align2::LEFT_CENTER,
223 };
224 pub const RIGHT_END: Self = Self {
226 parent: Align2::RIGHT_BOTTOM,
227 child: Align2::LEFT_BOTTOM,
228 };
229 pub const LEFT_START: Self = Self {
231 parent: Align2::LEFT_TOP,
232 child: Align2::RIGHT_TOP,
233 };
234 pub const LEFT: Self = Self {
236 parent: Align2::LEFT_CENTER,
237 child: Align2::RIGHT_CENTER,
238 };
239 pub const LEFT_END: Self = Self {
241 parent: Align2::LEFT_BOTTOM,
242 child: Align2::RIGHT_BOTTOM,
243 };
244
245 pub const PRESETS: [(Self, &'static str); 12] = [
248 (Self::TOP_START, "TOP_START"),
249 (Self::TOP, "TOP"),
250 (Self::TOP_END, "TOP_END"),
251 (Self::RIGHT_START, "RIGHT_START"),
252 (Self::RIGHT, "RIGHT"),
253 (Self::RIGHT_END, "RIGHT_END"),
254 (Self::BOTTOM_START, "BOTTOM_START"),
255 (Self::BOTTOM, "BOTTOM"),
256 (Self::BOTTOM_END, "BOTTOM_END"),
257 (Self::LEFT_START, "LEFT_START"),
258 (Self::LEFT, "LEFT"),
259 (Self::LEFT_END, "LEFT_END"),
260 ];
261
262 pub const MENU_ALIGNS: [Self; 12] = [
266 Self::BOTTOM_START,
267 Self::BOTTOM_END,
268 Self::TOP_START,
269 Self::TOP_END,
270 Self::RIGHT_END,
271 Self::RIGHT_START,
272 Self::LEFT_END,
273 Self::LEFT_START,
274 Self::TOP,
276 Self::RIGHT,
277 Self::BOTTOM,
278 Self::LEFT,
279 ];
280
281 pub fn preset_label(self) -> Option<&'static str> {
284 Self::PRESETS
285 .iter()
286 .find(|(a, _)| *a == self)
287 .map(|(_, label)| *label)
288 }
289
290 pub fn flip_x(self) -> Self {
292 Self {
293 parent: self.parent.flip_x(),
294 child: self.child.flip_x(),
295 }
296 }
297
298 pub fn flip_y(self) -> Self {
300 Self {
301 parent: self.parent.flip_y(),
302 child: self.child.flip_y(),
303 }
304 }
305
306 pub fn flip(self) -> Self {
308 Self {
309 parent: self.parent.flip(),
310 child: self.child.flip(),
311 }
312 }
313
314 pub fn symmetries(self) -> [Self; 3] {
317 [self.flip_x(), self.flip_y(), self.flip()]
318 }
319
320 pub fn find_best_align(
327 values_to_try: impl Iterator<Item = Self>,
328 content_rect: Rect,
329 parent_rect: Rect,
330 gap: f64,
331 expected_size: Size,
332 ) -> Option<Self> {
333 let mut first_choice = None;
334 for align in values_to_try {
335 first_choice = first_choice.or(Some(align));
336 let suggested = align.place_child(parent_rect, expected_size, gap);
337 if contains_rect(content_rect, suggested) {
338 return Some(align);
339 }
340 }
341 first_choice
342 }
343
344 pub fn place_child(self, parent: Rect, size: Size, gap: f64) -> Rect {
352 let (px, py) = self.parent.point_in(parent);
353 let gx = if self.parent.x != self.child.x {
354 self.parent.x.sign()
355 } else {
356 0.0
357 };
358 let gy = if self.parent.y != self.child.y {
359 self.parent.y.sign()
360 } else {
361 0.0
362 };
363 let ax = px + gap * gx;
364 let ay = py + gap * gy;
365 let cx = ax - size.width * self.child.x.frac();
366 let cy = ay - size.height * self.child.y.frac();
367 Rect::new(cx, cy, size.width, size.height)
368 }
369
370 pub fn place_child_clamped(self, parent: Rect, size: Size, gap: f64, viewport: Size) -> Rect {
374 clamp_rect(self.place_child(parent, size, gap), viewport)
375 }
376}
377
378fn contains_rect(outer: Rect, inner: Rect) -> bool {
380 inner.x >= outer.x
381 && inner.y >= outer.y
382 && inner.x + inner.width <= outer.x + outer.width
383 && inner.y + inner.height <= outer.y + outer.height
384}
385
386pub fn clamp_rect(rect: Rect, viewport: Size) -> Rect {
390 let max_x = (viewport.width - rect.width - MARGIN).max(MARGIN);
391 let max_y = (viewport.height - rect.height - MARGIN).max(MARGIN);
392 Rect::new(
393 rect.x.clamp(MARGIN, max_x),
394 rect.y.clamp(MARGIN, max_y),
395 rect.width,
396 rect.height,
397 )
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 const PARENT: Rect = Rect {
405 x: 100.0,
406 y: 100.0,
407 width: 40.0,
408 height: 20.0,
409 };
410 const SIZE: Size = Size {
411 width: 60.0,
412 height: 30.0,
413 };
414 const GAP: f64 = 4.0;
415
416 #[test]
417 fn bottom_start_hangs_below_left_aligned() {
418 let r = RectAlign::BOTTOM_START.place_child(PARENT, SIZE, GAP);
421 assert_eq!(r, Rect::new(100.0, 66.0, 60.0, 30.0));
422 assert_eq!(r.x, PARENT.x);
424 assert_eq!(r.y + r.height, PARENT.y - GAP);
426 }
427
428 #[test]
429 fn top_is_centered_above() {
430 let r = RectAlign::TOP.place_child(PARENT, SIZE, GAP);
431 assert_eq!(r.y, PARENT.y + PARENT.height + GAP);
433 let popup_center_x = r.x + r.width * 0.5;
435 let parent_center_x = PARENT.x + PARENT.width * 0.5;
436 assert!((popup_center_x - parent_center_x).abs() < 1e-9);
437 }
438
439 #[test]
440 fn right_is_centered_to_the_right() {
441 let r = RectAlign::RIGHT.place_child(PARENT, SIZE, GAP);
442 assert_eq!(r.x, PARENT.x + PARENT.width + GAP);
444 let popup_center_y = r.y + r.height * 0.5;
446 let parent_center_y = PARENT.y + PARENT.height * 0.5;
447 assert!((popup_center_y - parent_center_y).abs() < 1e-9);
448 }
449
450 #[test]
451 fn left_is_centered_to_the_left() {
452 let r = RectAlign::LEFT.place_child(PARENT, SIZE, GAP);
453 assert_eq!(r.x + r.width, PARENT.x - GAP);
455 let popup_center_y = r.y + r.height * 0.5;
456 let parent_center_y = PARENT.y + PARENT.height * 0.5;
457 assert!((popup_center_y - parent_center_y).abs() < 1e-9);
458 }
459
460 #[test]
461 fn zero_gap_puts_child_anchor_on_parent_anchor() {
462 let align = RectAlign {
465 parent: Align2::RIGHT_TOP,
466 child: Align2::LEFT_BOTTOM,
467 };
468 let r = align.place_child(PARENT, SIZE, 0.0);
469 let (px, py) = Align2::RIGHT_TOP.point_in(PARENT);
470 let (cx, cy) = Align2::LEFT_BOTTOM.point_in(r);
471 assert!((px - cx).abs() < 1e-9);
472 assert!((py - cy).abs() < 1e-9);
473 }
474
475 #[test]
476 fn clamp_keeps_popup_on_screen() {
477 let parent = Rect::new(2.0, 6.0, 40.0, 20.0);
479 let viewport = Size::new(400.0, 300.0);
480 let r = RectAlign::BOTTOM_START.place_child_clamped(parent, SIZE, GAP, viewport);
481 assert!(r.x >= 4.0);
482 assert!(r.y >= 4.0);
483 assert!(r.x + r.width <= viewport.width);
484 assert!(r.y + r.height <= viewport.height);
485 }
486
487 #[test]
488 fn presets_and_anchor_tables_round_trip() {
489 assert_eq!(RectAlign::BOTTOM.preset_label(), Some("BOTTOM"));
492 for (i, (a, _)) in Align2::ALL.iter().enumerate() {
493 assert_eq!(a.all_index(), i);
494 }
495 }
496
497 #[test]
498 fn flips_mirror_the_named_presets() {
499 assert_eq!(RectAlign::BOTTOM_START.flip_y(), RectAlign::TOP_START);
501 assert_eq!(RectAlign::TOP_END.flip_y(), RectAlign::BOTTOM_END);
502 assert_eq!(RectAlign::LEFT.flip_x(), RectAlign::RIGHT);
503 assert_eq!(RectAlign::RIGHT_START.flip_x(), RectAlign::LEFT_START);
504 assert_eq!(RectAlign::BOTTOM_START.flip(), RectAlign::TOP_END);
505 assert_eq!(
507 RectAlign::BOTTOM_START.symmetries(),
508 [
509 RectAlign::BOTTOM_END,
510 RectAlign::TOP_START,
511 RectAlign::TOP_END
512 ]
513 );
514 }
515
516 fn candidates(align: RectAlign) -> impl Iterator<Item = RectAlign> {
519 std::iter::once(align)
520 .chain(align.symmetries())
521 .chain(RectAlign::MENU_ALIGNS)
522 }
523
524 #[test]
525 fn find_best_align_flips_at_each_screen_edge() {
526 let screen = Rect::new(0.0, 0.0, 400.0, 300.0);
527 let size = Size::new(100.0, 80.0);
528
529 let parent = Rect::new(150.0, 10.0, 40.0, 20.0);
532 let best = RectAlign::find_best_align(
533 candidates(RectAlign::BOTTOM_START),
534 screen,
535 parent,
536 GAP,
537 size,
538 );
539 assert_eq!(best, Some(RectAlign::TOP_START));
540
541 let parent = Rect::new(150.0, 270.0, 40.0, 20.0);
543 let best =
544 RectAlign::find_best_align(candidates(RectAlign::TOP_START), screen, parent, GAP, size);
545 assert_eq!(best, Some(RectAlign::BOTTOM_START));
546
547 let parent = Rect::new(340.0, 100.0, 40.0, 20.0);
549 let best = RectAlign::find_best_align(
550 candidates(RectAlign::RIGHT_START),
551 screen,
552 parent,
553 GAP,
554 size,
555 );
556 assert_eq!(best, Some(RectAlign::LEFT_START));
557
558 let parent = Rect::new(10.0, 100.0, 40.0, 20.0);
560 let best = RectAlign::find_best_align(
561 candidates(RectAlign::LEFT_START),
562 screen,
563 parent,
564 GAP,
565 size,
566 );
567 assert_eq!(best, Some(RectAlign::RIGHT_START));
568 }
569
570 #[test]
571 fn find_best_align_keeps_preferred_align_when_it_fits() {
572 let screen = Rect::new(0.0, 0.0, 400.0, 300.0);
573 let parent = Rect::new(150.0, 150.0, 40.0, 20.0);
574 let best = RectAlign::find_best_align(
575 candidates(RectAlign::BOTTOM_START),
576 screen,
577 parent,
578 GAP,
579 Size::new(100.0, 80.0),
580 );
581 assert_eq!(best, Some(RectAlign::BOTTOM_START));
582 }
583
584 #[test]
585 fn find_best_align_falls_back_to_first_candidate_when_nothing_fits() {
586 let screen = Rect::new(0.0, 0.0, 100.0, 100.0);
589 let parent = Rect::new(40.0, 40.0, 20.0, 10.0);
590 let big = Size::new(500.0, 500.0);
591 let best = RectAlign::find_best_align(
592 candidates(RectAlign::BOTTOM_START),
593 screen,
594 parent,
595 GAP,
596 big,
597 );
598 assert_eq!(best, Some(RectAlign::BOTTOM_START));
599 assert_eq!(
600 RectAlign::find_best_align(std::iter::empty(), screen, parent, GAP, big),
601 None
602 );
603 }
604}