1use core::time::Duration;
24
25use blockworx_doc::{rev::Rev, trail::Trail};
26use blockworx_geom::Pos2;
27use egui::util::undoer::Undoer;
28
29use crate::{
30 canvas::Vantage,
31 path::BlockPath,
32 tools::{
33 SelectTool,
34 multi_pin_select::MultiPinSelect,
35 multi_select::MultiSelect,
36 resize_block::ResizeBlock,
37 tool::{Deletable, Tool, select_tool_for_anchor},
38 },
39 widget::drawing::Drawing,
40};
41pub const COALESCE: Duration = Duration::from_millis(1500);
45
46const DEPTH: usize = 100;
49
50#[derive(Clone, Debug, PartialEq)]
56pub struct Selection {
57 pub what: Deletable,
58 pub anchor: Option<Pos2>,
59}
60
61#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
72pub struct Stood(Rev);
73
74impl Stood {
75 pub fn of(trail: &Trail) -> Self {
76 Self(trail.standing())
77 }
78}
79
80#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
85pub enum Moved {
86 #[default]
87 Camera,
88 Fit,
89 Focus,
90 Scope,
91 Edit,
93}
94
95impl Moved {
96 pub fn label(self) -> &'static str {
99 match self {
100 Moved::Camera => "the camera move",
101 Moved::Fit => "zoom to fit",
102 Moved::Focus => "the focus",
103 Moved::Scope => "the change of scope",
104 Moved::Edit => "the edit",
105 }
106 }
107}
108
109#[derive(Clone, Debug)]
123pub struct State {
124 pub camera: Vantage,
125 pub scope: BlockPath,
126 pub stood: Stood,
127 pub moved: Moved,
128 pub selection: Option<Selection>,
129}
130
131impl PartialEq for State {
132 fn eq(&self, other: &Self) -> bool {
133 self.camera == other.camera && self.scope == other.scope && self.stood == other.stood
134 }
135}
136
137#[derive(Clone, Copy, PartialEq, Eq, Debug)]
140pub enum Kind {
141 Doc,
143 View,
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct Consequence {
151 pub target: String,
152 pub kind: Kind,
153}
154
155#[derive(Clone, Copy, PartialEq, Eq, Debug)]
158pub enum Direction {
159 Back,
160 Forward,
161}
162
163#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
168pub enum Recording {
169 #[default]
170 On,
171 Suspended,
172}
173
174pub struct UndoStack {
175 undoer: Undoer<State>,
176}
177
178impl UndoStack {
179 pub fn opening(at: &State) -> Self {
181 let mut stack = Self {
182 undoer: Undoer::with_settings(egui::util::undoer::Settings {
183 max_undos: DEPTH,
184 stable_time: COALESCE.as_secs_f32(),
185 ..Default::default()
186 }),
187 };
188 stack.undoer.add_undo(at);
189 stack
190 }
191
192 pub fn reconstructed(trail: &Trail, at: &State) -> Self {
203 let line = trail.standings();
204 let point = |depth: usize| State {
205 stood: Stood(line[depth]),
206 ..at.clone()
207 };
208 let mut stack = Self::opening(&point(0));
209 let past = trail.undo_depth();
210 let future = trail.redo_depth();
211 for depth in 1..=(past + future) {
212 stack.undoer.add_undo(&point(depth));
213 }
214 for depth in ((past + 1)..=(past + future)).rev() {
218 stack.undoer.undo(&point(depth));
219 }
220 stack
221 }
222
223 pub fn feed(&mut self, at: Duration, now: &State) {
226 self.undoer.feed_state(at.as_secs_f64(), now);
227 }
228
229 pub fn edited(&mut self, at: Duration, before: &State, now: &State) {
237 self.undoer.feed_state(at.as_secs_f64(), now);
238 self.undoer.add_undo(before);
239 self.undoer.add_undo(now);
240 }
241
242 pub fn landed(&mut self, at: &State) {
251 self.undoer.add_undo(at);
252 }
253
254 pub fn peek(&self, direction: Direction, now: &State) -> Option<State> {
261 let mut asked = UndoStack {
262 undoer: self.undoer.clone(),
263 };
264 asked.step(direction, now)
265 }
266
267 pub fn step(&mut self, direction: Direction, now: &State) -> Option<State> {
269 match direction {
270 Direction::Back => self.undoer.undo(now).cloned(),
271 Direction::Forward => self.undoer.redo(now).cloned(),
272 }
273 }
274}
275
276pub fn tool_for(data: &Drawing<'_>, candidates: &[&Selection]) -> Tool {
285 candidates
286 .iter()
287 .find_map(|selection| resolve(data, selection))
288 .unwrap_or_else(|| SelectTool.into())
289}
290
291fn resolve(data: &Drawing<'_>, selection: &Selection) -> Option<Tool> {
293 match &selection.what {
294 Deletable::Shape(shape) => data
295 .shape(*shape)
296 .is_some()
297 .then(|| ResizeBlock::Selected { shape: *shape }.into()),
298 Deletable::Route(id) => {
299 let anchor = selection.anchor?;
300 data.auto_route(*id)
301 .is_some()
302 .then(|| crate::tools::EditRoute::Selected { id: *id, anchor }.into())
303 }
304 Deletable::Shapes(shapes) => {
305 let live: Vec<_> = shapes
306 .iter()
307 .copied()
308 .filter(|id| data.shape(*id).is_some())
309 .collect();
310 match live.len() {
311 0 => None,
312 1 => Some(ResizeBlock::Selected { shape: live[0] }.into()),
313 _ => Some(MultiSelect::Selected { shapes: live }.into()),
314 }
315 }
316 Deletable::Pins(pins) => {
317 let live: Vec<_> = pins
318 .iter()
319 .copied()
320 .filter(|id| data.pin_on_shape(*id).is_some())
321 .collect();
322 match live.len() {
323 0 => None,
324 1 => Some(select_tool_for_anchor(data, live[0])),
325 _ => Some(MultiPinSelect::Selected { pins: live }.into()),
326 }
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use blockworx_doc::fixtures::{block_id, rev};
335 use blockworx_geom::{Vec2, pos2, vec2};
336
337 fn vantage(x: f32) -> Vantage {
338 Vantage {
339 zoom: blockworx_paint::Zoom::unity(),
340 translation: vec2(x, 0.0),
341 }
342 }
343
344 fn state() -> State {
345 State {
346 camera: Vantage {
347 zoom: blockworx_paint::Zoom::unity(),
348 translation: Vec2::ZERO,
349 },
350 scope: BlockPath::empty(),
351 stood: Stood::default(),
352 moved: Moved::default(),
353 selection: None,
354 }
355 }
356
357 fn looking_at(x: f32) -> State {
358 State {
359 camera: vantage(x),
360 ..state()
361 }
362 }
363
364 fn inside(n: u32) -> State {
365 let mut scope = BlockPath::empty();
366 scope.push(block_id(n));
367 State { scope, ..state() }
368 }
369
370 fn edited(depth: u64) -> State {
371 State {
372 stood: Stood(rev(depth)),
373 moved: Moved::Edit,
374 ..state()
375 }
376 }
377
378 fn shape(n: u32) -> Selection {
379 Selection {
380 what: Deletable::Shape(crate::shape::ShapeId::Rect(block_id(n))),
381 anchor: None,
382 }
383 }
384
385 fn later(at: Duration) -> Duration {
387 at + COALESCE + Duration::from_millis(1)
388 }
389
390 fn settle(stack: &mut UndoStack, at: Duration, now: &State) -> Duration {
393 stack.feed(at, now);
394 let at = later(at);
395 stack.feed(at, now);
396 at
397 }
398
399 #[test]
403 fn two_camera_moves_inside_the_window_are_one_entry_and_a_third_outside_is_a_second() {
404 let start = state();
405 let mut stack = UndoStack::opening(&start);
406 let mut at = Duration::ZERO;
407
408 stack.feed(at, &looking_at(10.0));
411 at += COALESCE / 3;
412 stack.feed(at, &looking_at(20.0));
413 at = settle(&mut stack, at, &looking_at(20.0));
414
415 stack.feed(at, &looking_at(30.0));
417 let at = settle(&mut stack, at, &looking_at(30.0));
418 assert!(at > COALESCE, "precondition: the third move is a new entry");
419
420 let here = looking_at(30.0);
421 let first = stack.step(Direction::Back, &here).expect("one step back");
422 assert_eq!(
423 first.camera,
424 vantage(20.0),
425 "the third move was buried in the pair before it",
426 );
427 let second = stack.step(Direction::Back, &first).expect("two steps back");
428 assert_eq!(
429 second.camera, start.camera,
430 "the two moves inside the window cost two presses instead of one",
431 );
432 assert!(
433 stack.peek(Direction::Back, &second).is_none(),
434 "the stack held more entries than the moves that were made",
435 );
436 }
437
438 #[test]
442 fn a_camera_worked_without_pause_leaves_one_entry() {
443 let start = state();
444 let mut stack = UndoStack::opening(&start);
445 let mut at = Duration::ZERO;
446 for step in 1..=40 {
447 at += COALESCE / 4;
448 stack.feed(at, &looking_at(step as f32));
449 }
450 let here = looking_at(40.0);
451 settle(&mut stack, at, &here);
452
453 let back = stack.step(Direction::Back, &here).expect("one step back");
454 assert_eq!(
455 back.camera, start.camera,
456 "a gesture's frames became entries of their own",
457 );
458 assert!(
459 stack.peek(Direction::Back, &back).is_none(),
460 "and only the one entry"
461 );
462 }
463
464 #[test]
467 fn doc_and_view_entries_come_back_in_the_order_they_were_made() {
468 let start = state();
469 let mut stack = UndoStack::opening(&start);
470 let at = Duration::ZERO;
471
472 let moved = looking_at(10.0);
473 let at = settle(&mut stack, at, &moved);
474 let one_edit = State {
475 camera: moved.camera,
476 ..edited(1)
477 };
478 stack.edited(at, &moved, &one_edit);
479 let two_edits = State {
480 camera: moved.camera,
481 ..edited(2)
482 };
483 stack.edited(at, &one_edit, &two_edits);
484 let wandered = State {
485 camera: vantage(20.0),
486 ..two_edits.clone()
487 };
488 let _ = settle(&mut stack, at, &wandered);
489
490 let mut here = wandered;
491 let mut walked = Vec::new();
492 while let Some(back) = stack.step(Direction::Back, &here) {
493 walked.push((back.stood, back.camera));
494 here = back;
495 }
496 assert_eq!(
497 walked,
498 vec![
499 (Stood(rev(2)), vantage(10.0)),
500 (Stood(rev(1)), vantage(10.0)),
501 (Stood(rev(0)), vantage(10.0)),
502 (Stood(rev(0)), start.camera),
503 ],
504 "the walk skipped a kind or reordered the two",
505 );
506 }
507
508 #[test]
512 fn edits_inside_the_coalescing_window_do_not_merge() {
513 let start = state();
514 let mut stack = UndoStack::opening(&start);
515 let at = Duration::ZERO;
516 let one = edited(1);
517 let two = edited(2);
518 stack.edited(at, &start, &one);
519 stack.edited(at + COALESCE / 10, &one, &two);
520
521 let back = stack.step(Direction::Back, &two).expect("one step back");
522 assert_eq!(back.stood, Stood(rev(1)), "one press took back two edits");
523 }
524
525 #[test]
528 fn redo_returns_to_where_undo_found_us_and_a_new_move_abandons_it() {
529 let start = state();
530 let mut stack = UndoStack::opening(&start);
531 let moved = looking_at(10.0);
532 let at = settle(&mut stack, Duration::ZERO, &moved);
533
534 let back = stack.step(Direction::Back, &moved).expect("a step back");
535 assert_eq!(back.camera, start.camera);
536 assert!(
537 stack.peek(Direction::Forward, &back).is_some(),
538 "the move is not on the forward half"
539 );
540 let forward = stack.step(Direction::Forward, &back).expect("a step on");
541 assert_eq!(forward.camera, moved.camera);
542
543 let back = stack.step(Direction::Back, &forward).expect("a step back");
544 let elsewhere = looking_at(99.0);
545 stack.feed(later(at), &elsewhere);
546 assert!(
547 stack.peek(Direction::Forward, &back).is_none(),
548 "a fresh move outlived the future it forked away from",
549 );
550 }
551
552 #[test]
556 fn a_scope_change_is_a_view_entry() {
557 let start = state();
558 let mut stack = UndoStack::opening(&start);
559 let deeper = inside(7);
560 assert_ne!(deeper.scope, start.scope, "precondition: the scope moved");
561 settle(&mut stack, Duration::ZERO, &deeper);
562
563 let back = stack.step(Direction::Back, &deeper).expect("a step back");
564 assert_eq!(back.scope, start.scope);
565 assert_eq!(back.stood, start.stood, "a scope change touched the log");
566 }
567
568 #[test]
571 fn a_reconstructed_stack_walks_the_trail_it_came_from() {
572 use blockworx_doc::trail::JournalAs;
573
574 let mut trail = Trail::default();
575 for at in 1..=3 {
576 trail.record(rev(at), JournalAs::Edit);
577 }
578 trail.record(rev(4), JournalAs::Undo { of: rev(3) });
579 assert_eq!(
580 (trail.undo_depth(), trail.redo_depth()),
581 (2, 1),
582 "precondition: the trail has depth both ways to reproduce",
583 );
584
585 let here = State {
586 stood: Stood::of(&trail),
587 ..state()
588 };
589 let mut stack = UndoStack::reconstructed(&trail, &here);
590 assert!(
591 stack.peek(Direction::Back, &here).is_some(),
592 "the reopened depth was not offered"
593 );
594 assert!(
595 stack.peek(Direction::Forward, &here).is_some(),
596 "the forward half was dropped"
597 );
598
599 let forward = stack.step(Direction::Forward, &here).expect("a step on");
600 assert_eq!(
601 forward.stood,
602 Stood(rev(3)),
603 "redo did not reach the trail's own future",
604 );
605 let mut walking = forward;
606 let mut stops = Vec::new();
607 while let Some(back) = stack.step(Direction::Back, &walking) {
608 stops.push(back.stood);
609 walking = back;
610 }
611 assert_eq!(
612 stops,
613 vec![Stood(rev(2)), Stood(rev(1)), Stood(rev(0))],
614 "the walk back does not match the trail it was built from",
615 );
616 }
617
618 #[test]
621 fn a_selection_change_is_not_an_entry_of_its_own() {
622 let start = state();
623 let mut stack = UndoStack::opening(&start);
624 let picked = State {
625 selection: Some(shape(4)),
626 ..state()
627 };
628 assert_eq!(picked, start, "the selection is outside the comparison");
629 settle(&mut stack, Duration::ZERO, &picked);
630 assert!(
631 stack.peek(Direction::Back, &picked).is_none(),
632 "picking something became something to take back",
633 );
634 }
635
636 #[test]
638 fn a_restored_selection_resolves_to_the_tool_that_holds_it() {
639 use crate::widget::test_fixtures::two_blocks_with_a_routed_waypoint;
640 let mut scene = two_blocks_with_a_routed_waypoint();
641 let drawing = scene.drawing();
642
643 assert!(
644 matches!(tool_for(&drawing, &[&shape(1)]), Tool::ResizeBlock(_)),
645 "a selected block restores to the tool that shows its overlay",
646 );
647 assert!(
648 matches!(tool_for(&drawing, &[]), Tool::Select(_)),
649 "no candidate restores to plain select",
650 );
651 }
652
653 #[test]
656 fn a_candidate_the_document_lost_gives_way_to_the_next() {
657 use crate::widget::test_fixtures::two_blocks_with_a_routed_waypoint;
658 let mut scene = two_blocks_with_a_routed_waypoint();
659 let drawing = scene.drawing();
660 let gone = shape(200);
661 assert!(
662 drawing
663 .shape(gone.what.shapes().expect("a shape")[0])
664 .is_none(),
665 "precondition: the document does not hold the first candidate",
666 );
667
668 assert!(
669 matches!(
670 tool_for(&drawing, &[&gone, &shape(1)]),
671 Tool::ResizeBlock(_)
672 ),
673 "the surviving candidate is taken",
674 );
675 assert!(
676 matches!(tool_for(&drawing, &[&gone]), Tool::Select(_)),
677 "and with none surviving, nothing is selected",
678 );
679 }
680
681 #[test]
685 fn a_route_candidate_without_its_anchor_is_passed_over() {
686 use crate::widget::test_fixtures::two_blocks_with_a_routed_waypoint;
687 let mut scene = two_blocks_with_a_routed_waypoint();
688 let drawing = scene.drawing();
689 let route = drawing
690 .auto_routes()
691 .next()
692 .map(|(id, _)| id)
693 .expect("the fixture's route");
694
695 let anchored = Selection {
696 what: Deletable::Route(route),
697 anchor: Some(pos2(30.0, 30.0)),
698 };
699 assert!(
700 matches!(tool_for(&drawing, &[&anchored]), Tool::EditRoute(_)),
701 "with its anchor, a route restores to the route editor",
702 );
703 let anchorless = Selection {
704 anchor: None,
705 ..anchored
706 };
707 assert!(matches!(
708 tool_for(&drawing, &[&anchorless]),
709 Tool::Select(_)
710 ));
711 }
712}