diff --git a/game/src/ltn/auto.rs b/game/src/ltn/auto.rs index 3426000f89..9711e5118d 100644 --- a/game/src/ltn/auto.rs +++ b/game/src/ltn/auto.rs @@ -55,11 +55,15 @@ impl Heuristic { // TODO If we already have no rat-runs, stop + app.session.modal_filters.before_edit(); + match self { Heuristic::Greedy => greedy(ctx, app, neighborhood, timer), Heuristic::BruteForce => brute_force(ctx, app, neighborhood, timer), Heuristic::OnlyOneBorder => only_one_border(app, neighborhood), } + + app.session.modal_filters.cancel_empty_edit(); } } diff --git a/game/src/ltn/connectivity.rs b/game/src/ltn/connectivity.rs index b13b98944f..508d8ac4d8 100644 --- a/game/src/ltn/connectivity.rs +++ b/game/src/ltn/connectivity.rs @@ -25,6 +25,9 @@ impl Viewer { app: &App, neighborhood: Neighborhood, ) -> Box> { + // TODO To handle undo. Going to switch to taking a NeighborhoodID instead! + let neighborhood = Neighborhood::new(ctx, app, neighborhood.orig_perimeter); + let mut viewer = Viewer { panel: Panel::empty(ctx), neighborhood, diff --git a/game/src/ltn/filters.rs b/game/src/ltn/filters.rs index 8d963795e2..0d8824609d 100644 --- a/game/src/ltn/filters.rs +++ b/game/src/ltn/filters.rs @@ -8,12 +8,15 @@ use widgetry::{Color, EventCtx, GeomBatch}; use super::Neighborhood; use crate::app::App; -/// Stored in App session state -#[derive(Default)] +/// Stored in App session state. Before making any changes, call `before_edit`. +#[derive(Clone, Default)] pub struct ModalFilters { /// For filters placed along a road, where is the filter located? pub roads: BTreeMap, pub intersections: BTreeMap, + + /// Edit history is preserved recursively + pub previous_version: Box>, } /// A diagonal filter exists in an intersection. It's defined by two roads (the order is @@ -34,6 +37,25 @@ pub struct DiagonalFilter { } impl ModalFilters { + /// Call before making any changes to preserve edit history + pub fn before_edit(&mut self) { + let copy = self.clone(); + self.previous_version = Box::new(Some(copy)); + } + + /// If it's possible no edits were made, undo the previous call to `before_edit` and collapse + /// the redundant piece of history. + pub fn cancel_empty_edit(&mut self) { + if let Some(prev) = self.previous_version.take() { + if self.roads == prev.roads && self.intersections == prev.intersections { + self.previous_version = prev.previous_version; + } else { + // There was a real difference, keep + self.previous_version = Box::new(Some(prev)); + } + } + } + /// Modify RoutingParams to respect these modal filters pub fn update_routing_params(&self, params: &mut RoutingParams) { params.avoid_roads.extend(self.roads.keys().cloned()); diff --git a/game/src/ltn/pathfinding.rs b/game/src/ltn/pathfinding.rs index 3337b20c87..05a9e5e94f 100644 --- a/game/src/ltn/pathfinding.rs +++ b/game/src/ltn/pathfinding.rs @@ -39,6 +39,9 @@ impl RoutePlanner { app: &mut App, neighborhood: Neighborhood, ) -> Box> { + // TODO To handle undo. Going to switch to taking a NeighborhoodID instead! + let neighborhood = Neighborhood::new(ctx, app, neighborhood.orig_perimeter); + let mut rp = RoutePlanner { panel: Panel::empty(ctx), waypoints: InputWaypoints::new(app), diff --git a/game/src/ltn/per_neighborhood.rs b/game/src/ltn/per_neighborhood.rs index a3933895bb..e090494b5b 100644 --- a/game/src/ltn/per_neighborhood.rs +++ b/game/src/ltn/per_neighborhood.rs @@ -62,15 +62,9 @@ impl Tab { ctx.style() .btn_plain .icon("system/assets/tools/undo.svg") - .disabled(true) + .disabled(app.session.modal_filters.previous_version.is_none()) .hotkey(lctrl(Key::Z)) .build_widget(ctx, "undo"), - ctx.style() - .btn_plain - .icon("system/assets/tools/redo.svg") - .disabled(true) - .hotkey(lctrl(Key::Y)) - .build_widget(ctx, "redo"), ]), ]) .section(ctx), @@ -107,36 +101,34 @@ impl Tab { state.take_neighborhood().orig_perimeter, )] })), - "Connectivity" => Transition::ConsumeState(Box::new(|state, ctx, app| { - let state = state.downcast::().ok().unwrap(); - vec![super::connectivity::Viewer::new_state( - ctx, - app, - state.take_neighborhood(), - )] - })), - "Rat runs" => Transition::ConsumeState(Box::new(|state, ctx, app| { - let state = state.downcast::().ok().unwrap(); - vec![super::rat_run_viewer::BrowseRatRuns::new_state( - ctx, - app, - state.take_neighborhood(), - )] - })), - "Pathfinding" => Transition::ConsumeState(Box::new(|state, ctx, app| { - let state = state.downcast::().ok().unwrap(); - vec![super::pathfinding::RoutePlanner::new_state( - ctx, - app, - state.take_neighborhood(), - )] - })), + "Connectivity" => Tab::Connectivity.switch_to_state::(), + "Rat runs" => Tab::RatRuns.switch_to_state::(), + "Pathfinding" => Tab::Pathfinding.switch_to_state::(), + "undo" => { + let prev = app.session.modal_filters.previous_version.take().unwrap(); + app.session.modal_filters = prev; + // Recreate the current state. This will reset any panel state (checkboxes and + // dropdowns) + self.switch_to_state::() + } _ => { return None; } }) } + fn switch_to_state>(self) -> Transition { + Transition::ConsumeState(Box::new(move |state, ctx, app| { + let state = state.downcast::().ok().unwrap(); + let n = state.take_neighborhood(); + vec![match self { + Tab::Connectivity => super::connectivity::Viewer::new_state(ctx, app, n), + Tab::RatRuns => super::rat_run_viewer::BrowseRatRuns::new_state(ctx, app, n), + Tab::Pathfinding => super::pathfinding::RoutePlanner::new_state(ctx, app, n), + }] + })) + } + fn make_buttons(self, ctx: &mut EventCtx) -> Widget { let mut row = Vec::new(); for (tab, label, key) in [ @@ -223,6 +215,7 @@ pub fn handle_world_outcome( return true; } + app.session.modal_filters.before_edit(); if app.session.modal_filters.roads.remove(&r).is_none() { // Place the filter on the part of the road that was clicked // These calls shouldn't fail -- since we clicked a road, the cursor must be in @@ -244,6 +237,7 @@ pub fn handle_world_outcome( } // Toggle through all possible filters + app.session.modal_filters.before_edit(); let mut all = DiagonalFilter::filters_for(app, i); if let Some(current) = app.session.modal_filters.intersections.get(&i) { let idx = all.iter().position(|x| x == current).unwrap(); diff --git a/game/src/ltn/rat_run_viewer.rs b/game/src/ltn/rat_run_viewer.rs index 194608b4d1..90469466a9 100644 --- a/game/src/ltn/rat_run_viewer.rs +++ b/game/src/ltn/rat_run_viewer.rs @@ -34,6 +34,9 @@ impl BrowseRatRuns { app: &App, neighborhood: Neighborhood, ) -> Box> { + // TODO To handle undo. Going to switch to taking a NeighborhoodID instead! + let neighborhood = Neighborhood::new(ctx, app, neighborhood.orig_perimeter); + let rat_runs = ctx.loading_screen("find rat runs", |_, timer| { find_rat_runs( &app.primary.map, @@ -158,7 +161,7 @@ impl State for BrowseRatRuns { self.recalculate(ctx, app); } x => { - return Tab::Connectivity + return Tab::RatRuns .handle_action::(ctx, app, x) .unwrap(); }