1use blockworx_geom::{Rect, Vec2, pos2};
10use blockworx_tools::commands::Precedence;
11
12pub const INLINE_CONTROLS: usize = 5;
16
17pub const CLEAR: f32 = 14.0;
19
20#[must_use]
26pub fn nothing_to_say(count: usize) -> &'static str {
27 if count > 1 {
28 "No actions shared by this selection"
29 } else {
30 "No actions for this selection"
31 }
32}
33
34pub enum Bar<T> {
40 Row(Vec<T>),
43 NothingShared,
46 Nothing,
49}
50
51impl<T> Bar<T> {
52 pub fn of(mut commands: Vec<T>, count: usize, precedence: impl Fn(&T) -> Precedence) -> Self {
55 if commands.is_empty() {
56 return if count > 1 {
57 Bar::NothingShared
58 } else {
59 Bar::Nothing
60 };
61 }
62 commands.sort_by_key(&precedence);
63 Bar::Row(commands)
64 }
65
66 #[must_use]
69 pub fn all(&self) -> &[T] {
70 match self {
71 Bar::Row(all) => all,
72 Bar::NothingShared | Bar::Nothing => &[],
73 }
74 }
75
76 #[must_use]
78 pub fn row(&self) -> &[T] {
79 self.all().split_at(self.cut()).0
80 }
81
82 #[must_use]
84 pub fn overflow(&self) -> &[T] {
85 self.all().split_at(self.cut()).1
86 }
87
88 #[must_use]
89 pub fn cut(&self) -> usize {
90 self.all().len().min(INLINE_CONTROLS)
91 }
92}
93
94#[must_use]
106pub fn place(sel: Rect, size: Vec2, region: Rect) -> Option<Rect> {
107 let band = |top: f32| Rect::from_min_size(pos2(sel.center().x - size.x / 2.0, top), size);
108 let fits = |bar: &Rect| bar.min.y >= region.min.y && bar.max.y <= region.max.y;
109 let outside = [band(sel.min.y - CLEAR - size.y), band(sel.max.y + CLEAR)]
110 .into_iter()
111 .find(fits);
112 let inside = || {
113 let bar = band(sel.min.y.max(region.min.y) + CLEAR);
114 fits(&bar).then_some(bar)
115 };
116 outside.or_else(inside).map(|bar| clamp(bar, region))
117}
118
119#[must_use]
123pub fn clamp(rect: Rect, region: Rect) -> Rect {
124 let shift = |low: f32, high: f32, min: f32, max: f32| {
125 if min < low {
126 low - min
127 } else if max > high {
128 (high - max).max(low - min)
129 } else {
130 0.0
131 }
132 };
133 rect.translate(Vec2::new(
134 shift(region.min.x, region.max.x, rect.min.x, rect.max.x),
135 shift(region.min.y, region.max.y, rect.min.y, rect.max.y),
136 ))
137}
138
139#[cfg(test)]
140mod tests {
141 use blockworx_geom::{pos2, vec2};
142
143 use super::*;
144
145 fn region() -> Rect {
148 Rect::from_min_size(pos2(0.0, 0.0), vec2(1000.0, 800.0))
149 }
150
151 const SIZE: Vec2 = Vec2::new(200.0, 40.0);
154
155 #[test]
156 fn places_the_bar_above_the_selection_with_the_specs_air() {
157 let sel = Rect::from_min_size(pos2(400.0, 400.0), vec2(100.0, 100.0));
158 let region = region();
159 assert!(
160 region.contains_rect(sel),
161 "precondition: the selection is inside the room the chrome left",
162 );
163 let bar = place(sel, SIZE, region).expect("a mid-canvas selection has room above it");
164 assert_eq!(bar.max.y, sel.min.y - CLEAR, "the air is not §3.3's 14px");
165 assert_eq!(
166 bar.center().x,
167 sel.center().x,
168 "the bar is not centred on it"
169 );
170 assert_eq!(bar.size(), SIZE, "placing resized the bar");
171 }
172
173 #[test]
174 fn flips_below_when_above_would_land_under_the_top_chrome() {
175 let region = Rect::from_min_max(pos2(0.0, 80.0), pos2(1000.0, 800.0));
176 let sel = Rect::from_min_size(pos2(400.0, 100.0), vec2(100.0, 100.0));
177 assert!(
178 sel.min.y - CLEAR - SIZE.y < region.min.y,
179 "precondition: above is under the chrome",
180 );
181 let bar = place(sel, SIZE, region).expect("there is room below it");
182 assert_eq!(bar.min.y, sel.max.y + CLEAR, "it did not flip below");
183 }
184
185 #[test]
186 fn clamps_sideways_into_the_room_the_chrome_left() {
187 let region = Rect::from_min_max(pos2(0.0, 0.0), pos2(688.0, 800.0));
188 let sel = Rect::from_min_size(pos2(640.0, 400.0), vec2(40.0, 40.0));
189 assert!(
190 sel.center().x + SIZE.x / 2.0 > region.max.x,
191 "precondition: centred, the bar would run under the navigator",
192 );
193 let bar = place(sel, SIZE, region).expect("a clamped bar still has a place");
194 assert!(
195 region.contains_rect(bar),
196 "the bar landed at {bar:?}, outside {region:?}",
197 );
198 assert_eq!(bar.size(), SIZE, "clamping resized the bar");
199 }
200
201 #[test]
206 fn the_bar_never_covers_what_is_selected() {
207 let region = Rect::from_min_max(pos2(92.0, 80.0), pos2(1000.0, 800.0));
208 let mut placed = 0;
209 for x in [-60.0, 0.0, 120.0, 500.0, 900.0, 980.0] {
210 for y in [-60.0, 0.0, 90.0, 400.0, 700.0, 780.0] {
211 for size in [vec2(24.0, 24.0), vec2(320.0, 180.0)] {
212 let sel = Rect::from_min_size(pos2(x, y), size);
213 let Some(bar) = place(sel, SIZE, region) else {
214 continue;
215 };
216 placed += 1;
217 assert!(
218 !bar.intersects(sel),
219 "the bar at {bar:?} covers the selection at {sel:?}",
220 );
221 assert!(
222 region.contains_rect(bar),
223 "the bar at {bar:?} left {region:?}",
224 );
225 }
226 }
227 }
228 assert!(
229 placed > 0,
230 "no selection was placed — the sweep proved nothing"
231 );
232 }
233
234 #[test]
240 fn a_selection_with_no_room_either_side_takes_the_band_inside_itself() {
241 let sel = Rect::from_min_size(pos2(-100.0, -100.0), vec2(1200.0, 1000.0));
242 let region = region();
243 assert!(
244 !region.contains_rect(sel),
245 "precondition: the selection overruns the region",
246 );
247 let placed = place(sel, SIZE, region).expect("an oversized selection still gets a bar");
248 assert!(
249 region.contains_rect(placed),
250 "the bar landed outside the region at {placed:?}",
251 );
252 assert!(
253 placed.min.y >= region.min.y + CLEAR,
254 "the bar sits flush against the region's own edge: {placed:?}",
255 );
256 }
257
258 #[test]
261 fn a_rect_too_big_for_the_region_aligns_to_its_near_edge() {
262 let region = Rect::from_min_max(pos2(100.0, 50.0), pos2(300.0, 150.0));
263 let wide = Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 400.0));
264 assert_eq!(clamp(wide, region).min, region.min);
265 }
266
267 #[test]
270 fn an_empty_bar_names_which_nothing_it_is() {
271 let none: Bar<u8> = Bar::of(Vec::new(), 1, |_: &u8| Precedence::Own);
272 assert!(matches!(none, Bar::Nothing));
273 assert!(none.all().is_empty());
274 let shared: Bar<u8> = Bar::of(Vec::new(), 3, |_: &u8| Precedence::Own);
275 assert!(matches!(shared, Bar::NothingShared));
276 assert_eq!(nothing_to_say(1), "No actions for this selection");
277 assert_eq!(nothing_to_say(3), "No actions shared by this selection");
278 }
279
280 #[test]
283 fn precedence_orders_the_bar_and_the_registry_orders_each_rank() {
284 let ranked = |at: &u8| {
285 if at.is_multiple_of(2) {
286 Precedence::Clerical
287 } else {
288 Precedence::Own
289 }
290 };
291 let bar = Bar::of(vec![0_u8, 1, 2, 3, 4, 5, 6], 1, ranked);
292 assert_eq!(bar.all(), [1, 3, 5, 0, 2, 4, 6]);
293 assert_eq!(bar.row().len(), INLINE_CONTROLS);
294 assert_eq!(bar.row(), [1, 3, 5, 0, 2]);
295 assert_eq!(bar.overflow(), [4, 6]);
296 }
297}