1use crate::geometry::{Point, ResizeHandle, Shape};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum GrabKind {
8 Move,
10 Resize(ResizeHandle),
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Selection {
16 pub shape: Shape,
17 pub label: String,
18 pub monitor: usize,
20 pub rot_deg: i32,
22}
23
24impl Selection {
25 pub const fn new(shape: Shape, monitor: usize) -> Self {
26 Self {
27 shape,
28 label: String::new(),
29 monitor,
30 rot_deg: 0,
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
38enum UndoOp {
39 RemoveLast,
40 Restack { from: usize, to: usize },
41 RemoveAt { index: usize },
42 Reinsert { index: usize, selection: Selection },
43 RestoreShape { index: usize, shape: Shape },
44 RestoreLabel { index: usize, label: String },
45 RestoreRotation { index: usize, deg: i32 },
46}
47
48#[derive(Debug, Default)]
49pub struct SelectionSet {
50 items: Vec<Selection>,
51 undo: Vec<UndoOp>,
52 redo: Vec<UndoOp>,
53}
54
55impl SelectionSet {
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn seed(items: Vec<Selection>) -> Self {
65 Self {
66 items,
67 undo: Vec::new(),
68 redo: Vec::new(),
69 }
70 }
71
72 pub fn items(&self) -> &[Selection] {
73 &self.items
74 }
75
76 pub fn len(&self) -> usize {
77 self.items.len()
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.items.is_empty()
82 }
83
84 pub fn get(&self, index: usize) -> Option<&Selection> {
85 self.items.get(index)
86 }
87
88 pub fn hit_topmost(&self, monitor: usize, p: Point) -> Option<usize> {
91 self.items
92 .iter()
93 .enumerate()
94 .rev()
95 .find(|(_, s)| s.monitor == monitor && s.shape.hit_test_rotated(s.rot_deg, p))
96 .map(|(i, _)| i)
97 }
98
99 pub fn grab_topmost(
103 &self,
104 monitor: usize,
105 p: Point,
106 tolerance: i32,
107 ) -> Option<(usize, GrabKind)> {
108 self.items
109 .iter()
110 .enumerate()
111 .rev()
112 .filter(|(_, s)| s.monitor == monitor)
113 .find_map(|(i, s)| {
114 if let Some(handle) = s.shape.resize_grab_rotated(s.rot_deg, p, tolerance) {
115 return Some((i, GrabKind::Resize(handle)));
116 }
117 if s.shape.hit_test_rotated(s.rot_deg, p) {
118 return Some((i, GrabKind::Move));
119 }
120 None
121 })
122 }
123
124 pub fn rotate(&mut self, index: usize, delta: i32) -> Option<i32> {
128 let s = self.items.get_mut(index)?;
129 if matches!(s.shape, Shape::Circle { .. }) || delta == 0 {
130 return None;
131 }
132 let previous = s.rot_deg;
133 s.rot_deg = crate::geometry::normalize_deg(s.rot_deg + delta);
134 self.undo.push(UndoOp::RestoreRotation {
135 index,
136 deg: previous,
137 });
138 self.redo.clear();
139 Some(self.items[index].rot_deg)
140 }
141
142 pub fn add(&mut self, selection: Selection) {
143 self.items.push(selection);
144 self.undo.push(UndoOp::RemoveLast);
145 self.redo.clear();
146 }
147
148 pub fn delete(&mut self, index: usize) -> bool {
149 if index >= self.items.len() {
150 return false;
151 }
152 let selection = self.items.remove(index);
153 self.undo.push(UndoOp::Reinsert { index, selection });
154 self.redo.clear();
155 true
156 }
157
158 pub fn set_shape_live(&mut self, index: usize, shape: Shape) {
161 if let Some(s) = self.items.get_mut(index) {
162 s.shape = shape;
163 }
164 }
165
166 pub fn commit_move(&mut self, index: usize, original: Shape) -> bool {
171 let Some(s) = self.items.get(index) else {
172 return false;
173 };
174 if s.shape == original {
175 return false;
176 }
177 self.undo.push(UndoOp::RestoreShape {
178 index,
179 shape: original,
180 });
181 self.redo.clear();
182 true
183 }
184
185 pub fn set_label(&mut self, index: usize, label: String) -> bool {
188 let Some(s) = self.items.get_mut(index) else {
189 return false;
190 };
191 if s.label == label {
192 return false;
193 }
194 let previous = std::mem::replace(&mut s.label, label);
195 self.undo.push(UndoOp::RestoreLabel {
196 index,
197 label: previous,
198 });
199 self.redo.clear();
200 true
201 }
202
203 pub fn cycle_at(&mut self, monitor: usize, p: Point) -> bool {
208 let hits: Vec<usize> = self
209 .items
210 .iter()
211 .enumerate()
212 .filter(|(_, s)| s.monitor == monitor && s.shape.hit_test_rotated(s.rot_deg, p))
213 .map(|(i, _)| i)
214 .collect();
215 let (Some(&bottom), Some(&top)) = (hits.first(), hits.last()) else {
216 return false;
217 };
218 if bottom == top {
219 return false;
220 }
221 let selection = self.items.remove(top);
222 self.items.insert(bottom, selection);
223 self.undo.push(UndoOp::Restack {
224 from: bottom,
225 to: top,
226 });
227 self.redo.clear();
228 true
229 }
230
231 pub fn undo(&mut self) -> bool {
234 let Some(op) = self.undo.pop() else {
235 return false;
236 };
237 if let Some(inverse) = self.apply(op) {
238 self.redo.push(inverse);
239 }
240 true
241 }
242
243 pub fn redo(&mut self) -> bool {
246 let Some(op) = self.redo.pop() else {
247 return false;
248 };
249 if let Some(inverse) = self.apply(op) {
250 self.undo.push(inverse);
251 }
252 true
253 }
254
255 fn apply(&mut self, op: UndoOp) -> Option<UndoOp> {
258 match op {
259 UndoOp::Restack { from, to } => {
260 if from >= self.items.len() {
261 return None;
262 }
263 let selection = self.items.remove(from);
264 let to = to.min(self.items.len());
265 self.items.insert(to, selection);
266 Some(UndoOp::Restack { from: to, to: from })
267 }
268 UndoOp::RemoveLast => {
269 let selection = self.items.pop()?;
270 Some(UndoOp::Reinsert {
271 index: self.items.len(),
272 selection,
273 })
274 }
275 UndoOp::RemoveAt { index } => {
276 if index >= self.items.len() {
277 return None;
278 }
279 let selection = self.items.remove(index);
280 Some(UndoOp::Reinsert { index, selection })
281 }
282 UndoOp::Reinsert { index, selection } => {
283 let index = index.min(self.items.len());
284 self.items.insert(index, selection);
285 Some(UndoOp::RemoveAt { index })
286 }
287 UndoOp::RestoreShape { index, shape } => {
288 let s = self.items.get_mut(index)?;
289 let previous = std::mem::replace(&mut s.shape, shape);
290 Some(UndoOp::RestoreShape {
291 index,
292 shape: previous,
293 })
294 }
295 UndoOp::RestoreLabel { index, label } => {
296 let s = self.items.get_mut(index)?;
297 let previous = std::mem::replace(&mut s.label, label);
298 Some(UndoOp::RestoreLabel {
299 index,
300 label: previous,
301 })
302 }
303 UndoOp::RestoreRotation { index, deg } => {
304 let s = self.items.get_mut(index)?;
305 let previous = std::mem::replace(&mut s.rot_deg, deg);
306 Some(UndoOp::RestoreRotation {
307 index,
308 deg: previous,
309 })
310 }
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::geometry::Rect;
319
320 fn rect_at(x: i32) -> Shape {
321 Shape::Rect(Rect::new(x, 0, 100, 100))
322 }
323
324 #[test]
325 fn hit_topmost_prefers_most_recent() {
326 let mut set = SelectionSet::new();
327 set.add(Selection::new(rect_at(0), 0));
328 set.add(Selection::new(rect_at(50), 0));
329 assert_eq!(set.hit_topmost(0, Point::new(60, 10)), Some(1));
331 assert_eq!(set.hit_topmost(0, Point::new(10, 10)), Some(0));
332 assert_eq!(set.hit_topmost(0, Point::new(500, 500)), None);
333 }
334
335 #[test]
336 fn hit_topmost_filters_by_monitor() {
337 let mut set = SelectionSet::new();
338 set.add(Selection::new(rect_at(0), 1));
339 assert_eq!(set.hit_topmost(0, Point::new(10, 10)), None);
340 assert_eq!(set.hit_topmost(1, Point::new(10, 10)), Some(0));
341 }
342
343 #[test]
344 fn undo_add_removes_it() {
345 let mut set = SelectionSet::new();
346 set.add(Selection::new(rect_at(0), 0));
347 assert!(set.undo());
348 assert!(set.is_empty());
349 }
350
351 #[test]
352 fn undo_delete_reinserts_at_original_position() {
353 let mut set = SelectionSet::new();
354 set.add(Selection::new(rect_at(0), 0));
355 set.add(Selection::new(rect_at(200), 0));
356 set.add(Selection::new(rect_at(400), 0));
357 assert!(set.delete(1));
358 assert_eq!(set.len(), 2);
359 assert!(set.undo());
360 assert_eq!(set.items()[1].shape, rect_at(200));
361 }
362
363 #[test]
364 fn undo_move_restores_original_shape() {
365 let mut set = SelectionSet::new();
366 set.add(Selection::new(rect_at(0), 0));
367 let original = set.items()[0].shape.clone();
368 set.set_shape_live(0, rect_at(300));
369 set.commit_move(0, original.clone());
370 assert!(set.undo());
371 assert_eq!(set.items()[0].shape, original);
372 }
373
374 #[test]
375 fn commit_move_without_change_records_nothing() {
376 let mut set = SelectionSet::new();
377 set.add(Selection::new(rect_at(0), 0));
378 let original = set.items()[0].shape.clone();
379 set.commit_move(0, original.clone());
380 assert!(set.undo());
382 assert!(set.is_empty());
383 assert!(!set.undo());
384 }
385
386 #[test]
387 fn noop_label_and_move_record_nothing() {
388 let mut set = SelectionSet::new();
389 set.add(Selection::new(rect_at(0), 0));
390 assert!(!set.set_label(0, String::new()));
392 assert!(set.set_label(0, "named".into()));
393 assert!(!set.set_label(0, "named".into()));
394 let original = set.items()[0].shape.clone();
396 assert!(!set.commit_move(0, original));
397 assert!(set.undo());
399 assert_eq!(set.items()[0].label, "");
400 assert!(set.undo());
401 assert!(!set.undo());
402 }
403
404 #[test]
405 fn undo_label_restores_previous_text() {
406 let mut set = SelectionSet::new();
407 set.add(Selection::new(rect_at(0), 0));
408 set.set_label(0, "first".into());
409 set.set_label(0, "second".into());
410 assert!(set.undo());
411 assert_eq!(set.items()[0].label, "first");
412 assert!(set.undo());
413 assert_eq!(set.items()[0].label, "");
414 }
415
416 #[test]
417 fn cycling_overlap_reaches_the_shape_beneath_and_undoes() {
418 let mut set = SelectionSet::new();
419 let mut below = Selection::new(Shape::Rect(Rect::new(0, 0, 100, 100)), 0);
420 below.label = "below".into();
421 let mut above = Selection::new(Shape::Rect(Rect::new(10, 10, 50, 50)), 0);
422 above.label = "above".into();
423 set.add(below);
424 set.add(above);
425 let p = Point::new(20, 20);
426 assert_eq!(set.items()[set.hit_topmost(0, p).unwrap()].label, "above");
427
428 assert!(set.cycle_at(0, p));
429 assert_eq!(
430 set.items()[set.hit_topmost(0, p).unwrap()].label,
431 "below",
432 "the shape beneath is now grabbable"
433 );
434 assert!(set.cycle_at(0, p));
436 assert_eq!(set.items()[set.hit_topmost(0, p).unwrap()].label, "above");
437 assert!(set.undo());
439 assert!(set.undo());
440 assert_eq!(set.items()[set.hit_topmost(0, p).unwrap()].label, "above");
441 assert_eq!(set.items()[0].label, "below", "original order restored");
442 }
443
444 #[test]
445 fn get_returns_the_selection_or_nothing() {
446 let mut set = SelectionSet::new();
447 set.add(Selection::new(Shape::Rect(Rect::new(1, 1, 2, 2)), 0));
448 assert!(set.get(0).is_some());
449 assert!(set.get(1).is_none());
450 }
451
452 #[test]
453 fn cycling_needs_at_least_two_shapes_under_the_point() {
454 let mut set = SelectionSet::new();
455 set.add(Selection::new(Shape::Rect(Rect::new(0, 0, 10, 10)), 0));
456 assert!(
457 !set.cycle_at(0, Point::new(5, 5)),
458 "one shape: nothing to cycle"
459 );
460 assert!(
461 !set.cycle_at(0, Point::new(50, 50)),
462 "no shape: nothing to cycle"
463 );
464 }
465
466 #[test]
467 fn seeding_restores_items_without_undo_history() {
468 let mut sel = Selection::new(Shape::Rect(Rect::new(1, 2, 3, 4)), 0);
469 sel.label = "kept".into();
470 sel.rot_deg = 30;
471 let mut set = SelectionSet::seed(vec![sel]);
472 assert_eq!(set.len(), 1);
473 assert_eq!(set.items()[0].label, "kept");
474 assert!(!set.undo(), "the resume point is the floor of history");
475 set.delete(0);
477 assert!(set.undo());
478 assert_eq!(set.len(), 1);
479 }
480
481 #[test]
482 fn redo_reapplies_undone_edits_across_every_op_kind() {
483 let mut set = SelectionSet::new();
484 set.add(Selection::new(Shape::Rect(Rect::new(0, 0, 10, 10)), 0));
485 set.set_label(0, "a".into());
486 set.rotate(0, 45);
487 set.set_shape_live(0, Shape::Rect(Rect::new(5, 5, 10, 10)));
488 set.commit_move(0, Shape::Rect(Rect::new(0, 0, 10, 10)));
489 set.delete(0);
490 while set.undo() {}
491 assert!(set.is_empty(), "everything unwinds");
492 while set.redo() {}
493 assert!(set.is_empty(), "the final delete replays too");
494 assert!(set.undo());
496 let s = &set.items()[0];
497 assert_eq!(s.label, "a");
498 assert_eq!(s.rot_deg, 45);
499 assert_eq!(s.shape, Shape::Rect(Rect::new(5, 5, 10, 10)));
500 }
501
502 #[test]
503 fn a_new_edit_empties_the_redo_branch() {
504 let mut set = SelectionSet::new();
505 set.add(Selection::new(Shape::Rect(Rect::new(0, 0, 10, 10)), 0));
506 assert!(set.undo());
507 set.add(Selection::new(Shape::Rect(Rect::new(9, 9, 5, 5)), 0));
508 assert!(!set.redo(), "a new edit forked history; redo is gone");
509 }
510
511 #[test]
512 fn redo_with_nothing_undone_is_false() {
513 let mut set = SelectionSet::new();
514 assert!(!set.redo());
515 set.add(Selection::new(Shape::Rect(Rect::new(0, 0, 4, 4)), 0));
516 assert!(!set.redo(), "un-undone edits leave nothing to redo");
517 }
518
519 #[test]
520 fn undo_is_lifo_across_mixed_ops() {
521 let mut set = SelectionSet::new();
522 set.add(Selection::new(rect_at(0), 0));
523 set.set_label(0, "a".into());
524 set.delete(0);
525 assert!(set.undo()); assert_eq!(set.items()[0].label, "a");
527 assert!(set.undo()); assert_eq!(set.items()[0].label, "");
529 assert!(set.undo()); assert!(set.is_empty());
531 assert!(!set.undo());
532 }
533
534 #[test]
535 fn delete_out_of_range_is_false() {
536 let mut set = SelectionSet::new();
537 assert!(!set.delete(0));
538 }
539
540 #[test]
541 fn rotate_records_undo_and_skips_circles() {
542 let mut set = SelectionSet::new();
543 set.add(Selection::new(rect_at(0), 0));
544 set.add(Selection::new(Shape::Circle { cx: 0, cy: 0, r: 9 }, 0));
545
546 assert_eq!(set.rotate(0, 15), Some(15));
547 assert_eq!(set.rotate(0, -30), Some(345));
548 assert_eq!(set.rotate(1, 15), None, "circles don't rotate");
549 assert_eq!(set.rotate(0, 0), None, "zero delta is a no-op");
550
551 assert!(set.undo());
552 assert_eq!(set.items()[0].rot_deg, 15);
553 assert!(set.undo());
554 assert_eq!(set.items()[0].rot_deg, 0);
555 }
556
557 #[test]
558 fn rotated_selection_hit_follows_rotation() {
559 let mut set = SelectionSet::new();
560 set.add(Selection::new(Shape::Rect(Rect::new(100, 100, 200, 20)), 0));
563 set.rotate(0, 90);
564 assert_eq!(set.hit_topmost(0, Point::new(200, 30)), Some(0));
565 assert_eq!(set.hit_topmost(0, Point::new(290, 110)), None);
566 }
567
568 #[test]
569 fn grab_prefers_border_resize_over_interior_move() {
570 let mut set = SelectionSet::new();
571 set.add(Selection::new(rect_at(0), 0)); let Some((0, GrabKind::Resize(_))) = set.grab_topmost(0, Point::new(1, 50), 5) else {
574 panic!("expected a resize grab on the edge");
575 };
576 assert_eq!(
578 set.grab_topmost(0, Point::new(50, 50), 5),
579 Some((0, GrabKind::Move))
580 );
581 assert_eq!(set.grab_topmost(0, Point::new(500, 500), 5), None);
583 }
584
585 #[test]
586 fn grab_topmost_shape_shadows_lower_border() {
587 let mut set = SelectionSet::new();
588 set.add(Selection::new(rect_at(0), 0)); set.add(Selection::new(Shape::Rect(Rect::new(50, 0, 200, 100)), 0)); assert_eq!(
593 set.grab_topmost(0, Point::new(100, 50), 5),
594 Some((1, GrabKind::Move))
595 );
596 }
597}