1use crate::geometry::{Rect, Size, clamp_u16};
4use crate::motion::steps;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum Placement {
10 #[default]
12 Below,
13 Above,
15 Right,
17 Left,
19}
20
21impl Placement {
22 pub const ALL: [Self; 4] = [Self::Below, Self::Above, Self::Right, Self::Left];
24
25 #[must_use]
27 pub fn name(self) -> &'static str {
28 match self {
29 Self::Below => "below",
30 Self::Above => "above",
31 Self::Right => "right",
32 Self::Left => "left",
33 }
34 }
35
36 pub(crate) fn opposite(self) -> Self {
38 match self {
39 Self::Below => Self::Above,
40 Self::Above => Self::Below,
41 Self::Right => Self::Left,
42 Self::Left => Self::Right,
43 }
44 }
45
46 fn vertical(self) -> bool {
47 matches!(self, Self::Below | Self::Above)
48 }
49}
50
51pub(crate) fn place(anchor: Rect, size: Size, screen: Rect, preferred: Placement) -> (Rect, Placement) {
58 let size = size.min(screen.size());
59 let (width, height) = (i32::from(size.width), i32::from(size.height));
60 let room = |side: Placement| match side {
61 Placement::Below => screen.bottom() - anchor.bottom(),
62 Placement::Above => anchor.y - screen.y,
63 Placement::Right => screen.right() - anchor.right(),
64 Placement::Left => anchor.x - screen.x,
65 };
66 let needed = |side: Placement| if side.vertical() { height } else { width };
67 let side = if room(preferred) >= needed(preferred) {
68 preferred
69 } else if room(preferred.opposite()) >= needed(preferred) {
70 preferred.opposite()
71 } else if !preferred.vertical() {
72 return place(anchor, size, screen, Placement::Below);
73 } else if room(preferred.opposite()) > room(preferred) {
74 preferred.opposite()
75 } else {
76 preferred
77 };
78 let clamp_x = |x: i32| x.min(screen.right() - width).max(screen.x);
79 let clamp_y = |y: i32| y.min(screen.bottom() - height).max(screen.y);
80 let (x, y) = match side {
81 Placement::Below => (clamp_x(anchor.x), clamp_y(anchor.bottom())),
82 Placement::Above => (clamp_x(anchor.x), clamp_y(anchor.y - height)),
83 Placement::Right => (clamp_x(anchor.right()), clamp_y(anchor.y)),
84 Placement::Left => (clamp_x(anchor.x - width), clamp_y(anchor.y)),
85 };
86 (Rect::new(x, y, clamp_u16(width), clamp_u16(height)), side)
87}
88
89pub(crate) fn unfold(full: Rect, side: Placement, progress: f32) -> Rect {
93 match side {
94 Placement::Below => Rect::new(full.x, full.y, full.width, steps(progress, full.height).max(1)),
95 Placement::Above => {
96 let rows = steps(progress, full.height).max(1);
97 Rect::new(full.x, full.bottom() - i32::from(rows), full.width, rows)
98 }
99 Placement::Right => Rect::new(full.x, full.y, steps(progress, full.width).max(1), full.height),
100 Placement::Left => {
101 let columns = steps(progress, full.width).max(1);
102 Rect::new(full.right() - i32::from(columns), full.y, columns, full.height)
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 const SCREEN: Rect = Rect::new(0, 0, 40, 20);
112
113 #[test]
114 fn uses_the_preferred_side_when_it_fits() {
115 let anchor = Rect::new(5, 3, 10, 1);
116 assert_eq!(
117 place(anchor, Size::new(12, 4), SCREEN, Placement::Below),
118 (Rect::new(5, 4, 12, 4), Placement::Below)
119 );
120 assert_eq!(
121 place(anchor, Size::new(12, 3), SCREEN, Placement::Above),
122 (Rect::new(5, 0, 12, 3), Placement::Above)
123 );
124 assert_eq!(
125 place(anchor, Size::new(8, 3), SCREEN, Placement::Right),
126 (Rect::new(15, 3, 8, 3), Placement::Right)
127 );
128 }
129
130 #[test]
131 fn flips_when_there_is_no_room() {
132 let bottom = Rect::new(5, 18, 10, 1);
133 assert_eq!(place(bottom, Size::new(12, 4), SCREEN, Placement::Below).1, Placement::Above);
134 let top = Rect::new(5, 1, 10, 1);
135 assert_eq!(place(top, Size::new(12, 4), SCREEN, Placement::Above).1, Placement::Below);
136 let right_edge = Rect::new(34, 5, 4, 1);
137 assert_eq!(
138 place(right_edge, Size::new(8, 3), SCREEN, Placement::Right),
139 (Rect::new(26, 5, 8, 3), Placement::Left)
140 );
141 }
142
143 #[test]
144 fn unfolds_away_from_the_anchor_in_whole_cells() {
145 let full = Rect::new(4, 6, 10, 4);
146 assert_eq!(unfold(full, Placement::Below, 0.5), Rect::new(4, 6, 10, 2));
147 assert_eq!(unfold(full, Placement::Above, 0.5), Rect::new(4, 8, 10, 2));
148 assert_eq!(unfold(full, Placement::Right, 0.3), Rect::new(4, 6, 3, 4));
149 assert_eq!(unfold(full, Placement::Left, 0.3), Rect::new(11, 6, 3, 4));
150 assert_eq!(unfold(full, Placement::Below, 0.0), Rect::new(4, 6, 10, 1), "one row shows at once");
151 assert_eq!(unfold(full, Placement::Left, 1.0), full);
152 }
153
154 #[test]
155 fn stays_on_screen() {
156 let corner = Rect::new(36, 10, 4, 1);
157 let (rect, _) = place(corner, Size::new(12, 4), SCREEN, Placement::Below);
158 assert_eq!(rect, Rect::new(28, 11, 12, 4));
159 let (tall, side) = place(Rect::new(2, 8, 4, 1), Size::new(6, 30), SCREEN, Placement::Below);
160 assert_eq!((tall.y, tall.height, side), (0, 20, Placement::Below));
161 let wide = place(Rect::new(18, 8, 4, 1), Size::new(30, 2), SCREEN, Placement::Right);
162 assert_eq!(wide.1, Placement::Below);
163 }
164}