Create a new widgetry transition to simplify state preservation in the LTN tool. It's maybe a bit specialized, but hopefully this kind of pattern could come in handy elsewhere, or a slightly more general pattern could emerge.

This commit is contained in:
Dustin Carlino 2022-05-19 14:21:44 +01:00
parent 36d3d85584
commit 7bb426f804
5 changed files with 78 additions and 73 deletions

View File

@ -5,17 +5,14 @@ use map_model::{IntersectionID, Perimeter};
use widgetry::tools::PolyLineLasso;
use widgetry::{DrawBaselayer, EventCtx, GfxCtx, Key, Line, ScreenPt, State, Text, Widget};
use crate::per_neighborhood::Tab;
use crate::{after_edit, App, DiagonalFilter, Neighborhood, NeighborhoodID, Transition};
use crate::{after_edit, App, DiagonalFilter, Neighborhood, Transition};
pub struct FreehandFilters {
lasso: PolyLineLasso,
id: NeighborhoodID,
perimeter: Perimeter,
interior_intersections: BTreeSet<IntersectionID>,
instructions: Text,
instructions_at: ScreenPt,
tab: Tab,
}
impl FreehandFilters {
@ -23,11 +20,9 @@ impl FreehandFilters {
ctx: &EventCtx,
neighborhood: &Neighborhood,
instructions_at: ScreenPt,
tab: Tab,
) -> Box<dyn State<App>> {
Box::new(Self {
lasso: PolyLineLasso::new(),
id: neighborhood.id,
perimeter: neighborhood.orig_perimeter.clone(),
interior_intersections: neighborhood.interior_intersections.clone(),
instructions_at,
@ -35,7 +30,6 @@ impl FreehandFilters {
Line("Click and drag").fg(ctx.style().text_hotkey_color),
Line(" across the roads you want to filter"),
]),
tab,
})
}
@ -80,16 +74,7 @@ impl State<App> for FreehandFilters {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
if let Some(pl) = self.lasso.event(ctx) {
self.make_filters_along_path(ctx, app, pl);
return Transition::Multi(vec![
Transition::Pop,
Transition::Replace(match self.tab {
Tab::Connectivity => crate::connectivity::Viewer::new_state(ctx, app, self.id),
// TODO Preserve the current shortcut
Tab::Shortcuts => {
crate::shortcut_viewer::BrowseShortcuts::new_state(ctx, app, self.id, None)
}
}),
]);
return Transition::Multi(vec![Transition::Pop, Transition::Recreate]);
}
Transition::Keep
}

View File

@ -98,7 +98,7 @@ impl State<App> for Viewer {
));
}
}
} else if let Some(t) = Tab::Connectivity.handle_action(
} else if let Some(t) = crate::per_neighborhood::handle_action(
ctx,
app,
x.as_ref(),
@ -167,6 +167,10 @@ impl State<App> for Viewer {
self.neighborhood.labels.draw(g, app);
}
}
fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
Self::new_state(ctx, app, self.neighborhood.id)
}
}
fn make_world(

View File

@ -12,6 +12,7 @@ use crate::{
after_edit, colors, App, BrowseNeighborhoods, DiagonalFilter, Neighborhood, Transition,
};
// TODO This is only used for styling now
#[derive(PartialEq)]
pub enum Tab {
Connectivity,
@ -68,60 +69,6 @@ impl Tab {
crate::components::LeftPanel::builder(ctx, top_panel, contents)
}
pub fn handle_action(
self,
ctx: &mut EventCtx,
app: &mut App,
action: &str,
neighborhood: &Neighborhood,
panel: &Panel,
) -> Option<Transition> {
let id = neighborhood.id;
match action {
"Browse neighborhoods" => {
// Recalculate the state to redraw any changed filters
Some(Transition::Replace(BrowseNeighborhoods::new_state(
ctx, app,
)))
}
"Adjust boundary" => Some(Transition::Replace(
crate::select_boundary::SelectBoundary::new_state(ctx, app, id),
)),
"Connectivity" => Some(Transition::Replace(crate::connectivity::Viewer::new_state(
ctx, app, id,
))),
"Shortcuts" => Some(Transition::Replace(
crate::shortcut_viewer::BrowseShortcuts::new_state(ctx, app, id, None),
)),
"Create filters along a shape" => Some(Transition::Push(
crate::components::FreehandFilters::new_state(
ctx,
neighborhood,
panel.center_of("Create filters along a shape"),
self,
),
)),
"undo" => {
let prev = app.session.modal_filters.previous_version.take().unwrap();
app.session.modal_filters = prev;
after_edit(ctx, app);
// Recreate the current state. This will reset any panel state (checkboxes and
// dropdowns)
Some(Transition::Replace(match self {
Tab::Connectivity => crate::connectivity::Viewer::new_state(ctx, app, id),
// TODO Preserve the current shortcut
Tab::Shortcuts => {
crate::shortcut_viewer::BrowseShortcuts::new_state(ctx, app, id, None)
}
}))
}
"Plan a route" => Some(Transition::Push(
crate::route_planner::RoutePlanner::new_state(ctx, app),
)),
_ => None,
}
}
fn make_buttons(self, ctx: &mut EventCtx, app: &App) -> Widget {
let mut row = Vec::new();
for (tab, label, key) in [
@ -166,6 +113,51 @@ impl Tab {
}
}
pub fn handle_action(
ctx: &mut EventCtx,
app: &mut App,
action: &str,
neighborhood: &Neighborhood,
panel: &Panel,
) -> Option<Transition> {
let id = neighborhood.id;
match action {
"Browse neighborhoods" => {
// Recalculate the state to redraw any changed filters
Some(Transition::Replace(BrowseNeighborhoods::new_state(
ctx, app,
)))
}
"Adjust boundary" => Some(Transition::Replace(
crate::select_boundary::SelectBoundary::new_state(ctx, app, id),
)),
"Connectivity" => Some(Transition::Replace(crate::connectivity::Viewer::new_state(
ctx, app, id,
))),
"Shortcuts" => Some(Transition::Replace(
crate::shortcut_viewer::BrowseShortcuts::new_state(ctx, app, id, None),
)),
"Create filters along a shape" => Some(Transition::Push(
crate::components::FreehandFilters::new_state(
ctx,
neighborhood,
panel.center_of("Create filters along a shape"),
),
)),
"undo" => {
let prev = app.session.modal_filters.previous_version.take().unwrap();
app.session.modal_filters = prev;
after_edit(ctx, app);
// TODO Ideally, preserve panel state (checkboxes and dropdowns)
Some(Transition::Recreate)
}
"Plan a route" => Some(Transition::Push(
crate::route_planner::RoutePlanner::new_state(ctx, app),
)),
_ => None,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FilterableObj {
InteriorRoad(RoadID),

View File

@ -174,7 +174,7 @@ impl State<App> for BrowseShortcuts {
self.recalculate(ctx, app);
}
x => {
if let Some(t) = Tab::Shortcuts.handle_action(
if let Some(t) = crate::per_neighborhood::handle_action(
ctx,
app,
x,
@ -235,6 +235,15 @@ impl State<App> for BrowseShortcuts {
self.neighborhood.labels.draw(g, app);
}
}
fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
let current_request = if self.shortcuts.paths.is_empty() {
None
} else {
Some(self.shortcuts.paths[self.current_idx].get_req().clone())
};
Self::new_state(ctx, app, self.neighborhood.id, current_request)
}
}
fn help() -> Vec<&'static str> {

View File

@ -144,6 +144,13 @@ impl<A: SharedAppState> App<A> {
self.states.extend(states);
true
}
Transition::Recreate => {
// TODO Don't call on_destroy?
let mut last = self.states.pop().unwrap();
let replacement = last.recreate(ctx, &mut self.shared_app_state);
self.states.push(replacement);
true
}
Transition::Multi(list) => {
// Always wake-up just the last state remaining after the sequence
for t in list {
@ -182,6 +189,12 @@ pub trait State<A>: downcast_rs::Downcast {
/// Before this state is popped or replaced, call this.
fn on_destroy(&mut self, _: &mut EventCtx, _: &mut A) {}
// We don't need an on_enter -- the constructor for the state can just do it.
/// Respond to `Transition::Recreate` by assuming state in the app has changed, but preserving
/// the `State`-specific state appropriately.
fn recreate(&mut self, _: &mut EventCtx, _: &mut A) -> Box<dyn State<A>> {
panic!("This state hasn't implemented support for Transition::Recreate");
}
}
downcast_rs::impl_downcast!(State<A>);
@ -210,6 +223,8 @@ pub enum Transition<A> {
Replace(Box<dyn State<A>>),
/// Replace the entire stack of states with this stack.
Clear(Vec<Box<dyn State<A>>>),
/// Call `State::recreate` on the current top of the stack
Recreate,
/// Execute a sequence of transitions in order.
Multi(Vec<Transition<A>>),
}