make a way simpler, one-choice version of Wizard. use it in two

different places to flesh out how it works
This commit is contained in:
Dustin Carlino 2020-08-06 09:46:57 -07:00
parent 5eefdaef18
commit c41c998301
5 changed files with 149 additions and 70 deletions

View File

@ -464,4 +464,10 @@ impl Choice<String> {
pub fn string(label: &str) -> Choice<String> {
Choice::new(label.to_string(), label.to_string())
}
pub fn strings(list: Vec<String>) -> Vec<Choice<String>> {
list.into_iter()
.map(|x| Choice::new(x.clone(), x))
.collect()
}
}

View File

@ -128,13 +128,8 @@ impl<T: 'static + Clone> WidgetImpl for Menu<T> {
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return;
}
// Unconsume the click, it was in screen space, but not on us.
ctx.input.unconsume_event();
} else {
// Clicked on the map? Cancel out
self.state = InputResult::Canceled;
return;
}
ctx.input.unconsume_event();
}
}

View File

@ -1,11 +1,11 @@
use crate::app::App;
use crate::colors::ColorScheme;
use crate::game::{State, Transition, WizardState};
use crate::game::{ChooseSomething, State, Transition};
use aabb_quadtree::QuadTree;
use abstutil::{prettyprint_usize, Parallelism};
use ezgui::{
hotkey, lctrl, Btn, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, Text, TextExt, VerticalAlignment, Widget, Wizard,
HorizontalAlignment, Key, Line, Outcome, Text, TextExt, VerticalAlignment, Widget,
};
use geom::{Circle, Distance, PolyLine, Polygon, Pt2D, Ring};
use kml::ExtraShapes;
@ -166,7 +166,22 @@ impl State for ViewKML {
return Transition::Pop;
}
"load KML file" => {
return Transition::Push(WizardState::new(Box::new(choose_kml)));
return Transition::Push(ChooseSomething::new(
ctx,
"Load file",
Choice::strings(
abstutil::list_dir(std::path::Path::new(&abstutil::path(format!(
"input/{}/",
app.primary.map.get_city_name()
))))
.into_iter()
.filter(|x| x.ends_with(".bin") && !x.ends_with("popdat.bin"))
.collect(),
),
Box::new(|path, ctx, app| {
Transition::Replace(ViewKML::new(ctx, app, Some(path)))
}),
));
}
_ => unreachable!(),
},
@ -332,16 +347,3 @@ fn make_query(app: &App, objects: &Vec<Object>, query: &str) -> (GeomBatch, usiz
}
(batch, cnt)
}
fn choose_kml(wiz: &mut Wizard, ctx: &mut EventCtx, app: &mut App) -> Option<Transition> {
let path = wiz.wrap(ctx).choose_string("View what KML dataset?", || {
abstutil::list_dir(std::path::Path::new(&abstutil::path(format!(
"input/{}/",
app.primary.map.get_city_name()
))))
.into_iter()
.filter(|x| x.ends_with(".bin") && !x.ends_with("popdat.bin"))
.collect()
})?;
Some(Transition::Replace(ViewKML::new(ctx, app, Some(path))))
}

View File

@ -1,6 +1,6 @@
use crate::app::{App, ShowEverything};
use crate::common::CommonState;
use crate::game::{DrawBaselayer, State, Transition, WizardState};
use crate::game::{ChooseSomething, DrawBaselayer, State, Transition, WizardState};
use crate::render::DrawOptions;
use ezgui::{
hotkey, lctrl, Btn, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
@ -198,52 +198,46 @@ impl State for StoryMapEditor {
}
"load" => {
// TODO autosave
let current = self.story.name.clone();
let btn = self.composite.rect_of("load").clone();
return Transition::Push(WizardState::new(Box::new(move |wiz, ctx, app| {
let (_, raw) = wiz.wrap(ctx).choose_exact(
(
HorizontalAlignment::Centered(btn.center().x),
VerticalAlignment::Below(btn.y2 + 15.0),
),
None,
|| {
let mut list = Vec::new();
for (name, story) in abstutil::load_all_objects::<RecordedStoryMap>(
abstutil::path("player/stories"),
) {
if story.name == current {
continue;
}
// TODO Argh, we can't make StoryMap cloneable, so redo some
// work
let gps_bounds = app.primary.map.get_gps_bounds();
if story
.markers
.iter()
.all(|(pts, _)| gps_bounds.try_convert(pts).is_some())
{
list.push(Choice::new(name, story));
}
}
list.push(Choice::new(
"new story",
RecordedStoryMap {
name: "new story".to_string(),
markers: Vec::new(),
},
));
list
},
)?;
let story = StoryMap::load(ctx, app, raw).unwrap();
Some(Transition::PopWithData(Box::new(move |state, ctx, _| {
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
editor.story = story;
editor.dirty = false;
editor.redo_panel(ctx);
})))
})));
let mut choices = Vec::new();
for (name, story) in abstutil::load_all_objects::<RecordedStoryMap>(
abstutil::path("player/stories"),
) {
if story.name == self.story.name {
continue;
}
// TODO Argh, we can't make StoryMap cloneable, so redo some
// work
let gps_bounds = app.primary.map.get_gps_bounds();
if story
.markers
.iter()
.all(|(pts, _)| gps_bounds.try_convert(pts).is_some())
{
choices.push(Choice::new(name, story));
}
}
choices.push(Choice::new(
"new story",
RecordedStoryMap {
name: "new story".to_string(),
markers: Vec::new(),
},
));
return Transition::Push(ChooseSomething::new_below(
ctx,
self.composite.rect_of("load"),
choices,
Box::new(|raw, ctx, app| {
let story = StoryMap::load(ctx, app, raw).unwrap();
Transition::PopWithData(Box::new(move |state, ctx, _| {
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
editor.story = story;
editor.dirty = false;
editor.redo_panel(ctx);
}))
}),
));
}
"new marker" => {
self.hovering = None;

View File

@ -3,7 +3,10 @@ use crate::options::Options;
use crate::pregame::TitleScreen;
use crate::render::DrawOptions;
use crate::sandbox::{GameplayMode, SandboxMode};
use ezgui::{Canvas, Drawable, EventCtx, GfxCtx, Wizard, GUI};
use ezgui::{
hotkey, Btn, Canvas, Choice, Composite, Drawable, EventCtx, GfxCtx, HorizontalAlignment, Key,
Line, Menu, Outcome, ScreenRectangle, VerticalAlignment, Widget, Wizard, GUI,
};
use geom::Polygon;
use map_model::PermanentMapEdits;
@ -357,3 +360,82 @@ pub fn msg<S: Into<String>>(title: &'static str, lines: Vec<S>) -> Box<dyn State
Some(Transition::Pop)
}))
}
pub struct ChooseSomething<T> {
composite: Composite,
cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>,
}
impl<T: 'static + Clone> ChooseSomething<T> {
pub fn new(
ctx: &mut EventCtx,
query: &str,
choices: Vec<Choice<T>>,
cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>,
) -> Box<dyn State> {
Box::new(ChooseSomething {
composite: Composite::new(Widget::col(vec![
Widget::row(vec![
Line(query).small_heading().draw(ctx),
Btn::plaintext("X")
.build(ctx, "close", hotkey(Key::Escape))
.align_right(),
]),
Menu::new(ctx, choices).named("menu"),
]))
.build(ctx),
cb,
})
}
pub fn new_below(
ctx: &mut EventCtx,
rect: &ScreenRectangle,
choices: Vec<Choice<T>>,
cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>,
) -> Box<dyn State> {
Box::new(ChooseSomething {
composite: Composite::new(Menu::new(ctx, choices).named("menu").container())
.aligned(
HorizontalAlignment::Centered(rect.center().x),
VerticalAlignment::Below(rect.y2 + 15.0),
)
.build(ctx),
cb,
})
}
}
impl<T: 'static + Clone> State for ChooseSomething<T> {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
match self.composite.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"close" => Transition::Pop,
_ => {
// TODO We shouldn't need to clone everywhere
let data = self.composite.menu::<T>("menu").current_choice().clone();
(self.cb)(data, ctx, app)
}
},
_ => {
if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
return Transition::Pop;
}
// new_below doesn't make an X button
if ctx.input.key_pressed(Key::Escape) {
return Transition::Pop;
}
Transition::Keep
}
}
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::PreviousState
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
State::grey_out_map(g, app);
self.composite.draw(g);
}
}