move more UIs away from wizards. editing signal offset broke, because

the perma format doesn't encode offset yet
This commit is contained in:
Dustin Carlino 2020-08-07 13:33:58 -07:00
parent ec756b5167
commit f61881f6fa
8 changed files with 416 additions and 259 deletions

View File

@ -465,9 +465,12 @@ impl Choice<String> {
Choice::new(label.to_string(), label.to_string())
}
pub fn strings(list: Vec<String>) -> Vec<Choice<String>> {
pub fn strings<I: Into<String>>(list: Vec<I>) -> Vec<Choice<String>> {
list.into_iter()
.map(|x| Choice::new(x.clone(), x))
.map(|x| {
let x = x.into();
Choice::new(x.clone(), x)
})
.collect()
}
}

View File

@ -5,14 +5,14 @@ pub mod shared_row;
use crate::app::{App, ShowLayers, ShowObject};
use crate::common::{tool_panel, CommonState, ContextualActions};
use crate::game::{msg, ChooseSomething, DrawBaselayer, State, Transition, WizardState};
use crate::game::{msg, ChooseSomething, DrawBaselayer, PromptInput, State, Transition};
use crate::helpers::ID;
use crate::options::OptionsPanel;
use crate::render::{calculate_corners, DrawOptions};
use abstutil::{Parallelism, Tags, Timer};
use ezgui::{
hotkey, lctrl, Btn, Checkbox, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, Text, UpdateType, VerticalAlignment, Widget, Wizard,
HorizontalAlignment, Key, Line, Outcome, Text, UpdateType, VerticalAlignment, Widget,
};
use geom::{Distance, Pt2D};
use map_model::{osm, ControlTrafficSignal, NORMAL_LANE_THICKNESS};
@ -211,7 +211,11 @@ impl State for DebugMode {
self.reset_info(ctx);
}
"search OSM metadata" => {
return Transition::Push(WizardState::new(Box::new(search_osm)));
return Transition::Push(PromptInput::new(
ctx,
"Search for what?",
Box::new(search_osm),
));
}
"clear OSM search results" => {
self.search_results = None;
@ -363,8 +367,7 @@ impl ShowObject for DebugMode {
}
}
fn search_osm(wiz: &mut Wizard, ctx: &mut EventCtx, app: &mut App) -> Option<Transition> {
let filter = wiz.wrap(ctx).input_string("Search for what?")?;
fn search_osm(filter: String, ctx: &mut EventCtx, app: &mut App) -> Transition {
let mut num_matches = 0;
let mut batch = GeomBatch::new();
@ -398,11 +401,11 @@ fn search_osm(wiz: &mut Wizard, ctx: &mut EventCtx, app: &mut App) -> Option<Tra
draw: batch.upload(ctx),
};
Some(Transition::PopWithData(Box::new(|state, ctx, _| {
Transition::PopWithData(Box::new(|state, ctx, _| {
let mut mode = state.downcast_mut::<DebugMode>().unwrap();
mode.search_results = Some(results);
mode.reset_info(ctx);
})))
}))
}
struct SearchResults {

View File

@ -1,11 +1,11 @@
use crate::app::{App, ShowEverything};
use crate::common::{CityPicker, ColorLegend};
use crate::game::{msg, State, Transition, WizardState};
use crate::game::{msg, State, Transition};
use crate::helpers::{nice_map_name, open_browser, ID};
use abstutil::{prettyprint_usize, Tags, Timer};
use ezgui::{
hotkey, Btn, Checkbox, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, Text, TextExt, VerticalAlignment, Widget,
HorizontalAlignment, Key, Line, Menu, Outcome, Text, TextExt, VerticalAlignment, Widget,
};
use geom::{Distance, FindClosest, PolyLine, Polygon};
use map_model::{osm, RoadID};
@ -20,7 +20,6 @@ pub struct ParkingMapper {
draw_layer: Drawable,
show: Show,
selected: Option<(HashSet<RoadID>, Drawable)>,
hide_layer: bool,
data: BTreeMap<i64, Value>,
}
@ -182,75 +181,9 @@ impl ParkingMapper {
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
.build(ctx),
selected: None,
hide_layer: false,
data,
})
}
fn make_wizard(&self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State> {
let show = self.show;
let osm_way_id = app
.primary
.map
.get_r(*self.selected.as_ref().unwrap().0.iter().next().unwrap())
.orig_id
.osm_way_id;
let data = self.data.clone();
let mut state = WizardState::new(Box::new(move |wiz, ctx, app| {
let mut wizard = wiz.wrap(ctx);
let (_, value) = wizard.choose("What kind of parking does this road have?", || {
vec![
Choice::new("none -- no stopping or parking", Value::NoStopping),
Choice::new("both sides", Value::BothSides),
Choice::new("just on the green side", Value::RightOnly),
Choice::new("just on the blue side", Value::LeftOnly),
Choice::new(
"it changes at some point along the road",
Value::Complicated,
),
Choice::new("loading zone on one or both sides", Value::Complicated),
]
})?;
if value == Value::Complicated {
wizard.acknowledge("Complicated road", || {
vec![
"You'll have to manually split the way in ID or JOSM and apply the \
appropriate parking tags to each section.",
]
})?;
}
let mut new_data = data.clone();
new_data.insert(osm_way_id, value);
Some(Transition::PopThenReplace(ParkingMapper::make(
ctx, app, show, new_data,
)))
}));
state.downcast_mut::<WizardState>().unwrap().custom_pop = Some(Transition::PopThenReplace(
ParkingMapper::make(ctx, app, self.show, self.data.clone()),
));
let mut batch = GeomBatch::new();
let map = &app.primary.map;
let thickness = Distance::meters(2.0);
for id in &self.selected.as_ref().unwrap().0 {
let r = map.get_r(*id);
batch.push(
Color::GREEN,
map.right_shift(r.center_pts.clone(), r.get_half_width(map))
.make_polygons(thickness),
);
batch.push(
Color::BLUE,
map.left_shift(r.center_pts.clone(), r.get_half_width(map))
.make_polygons(thickness),
);
}
state.downcast_mut::<WizardState>().unwrap().also_draw =
Some((ctx.upload(batch.clone()), ctx.upload(batch)));
state
}
}
impl State for ParkingMapper {
@ -340,8 +273,13 @@ impl State for ParkingMapper {
}
}
if self.selected.is_some() && app.per_obj.left_click(ctx, "map parking") {
self.hide_layer = true;
return Transition::Push(self.make_wizard(ctx, app));
return Transition::Push(ChangeWay::new(
ctx,
app,
&self.selected.as_ref().unwrap().0,
self.show,
self.data.clone(),
));
}
if self.selected.is_some() && ctx.input.key_pressed(Key::N) {
let osm_way_id = app
@ -437,9 +375,7 @@ impl State for ParkingMapper {
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
if !self.hide_layer {
g.redraw(&self.draw_layer);
}
g.redraw(&self.draw_layer);
if let Some((_, ref roads)) = self.selected {
g.redraw(roads);
}
@ -447,6 +383,125 @@ impl State for ParkingMapper {
}
}
struct ChangeWay {
composite: Composite,
draw: Drawable,
osm_way_id: i64,
data: BTreeMap<i64, Value>,
show: Show,
}
impl ChangeWay {
fn new(
ctx: &mut EventCtx,
app: &App,
selected: &HashSet<RoadID>,
show: Show,
data: BTreeMap<i64, Value>,
) -> Box<dyn State> {
let map = &app.primary.map;
let osm_way_id = map
.get_r(*selected.iter().next().unwrap())
.orig_id
.osm_way_id;
let mut batch = GeomBatch::new();
let thickness = Distance::meters(2.0);
for id in selected {
let r = map.get_r(*id);
batch.push(
Color::GREEN,
map.right_shift(r.center_pts.clone(), r.get_half_width(map))
.make_polygons(thickness),
);
batch.push(
Color::BLUE,
map.left_shift(r.center_pts.clone(), r.get_half_width(map))
.make_polygons(thickness),
);
}
Box::new(ChangeWay {
composite: Composite::new(Widget::col(vec![
Widget::row(vec![
Line("What kind of parking does this road have?")
.small_heading()
.draw(ctx),
Btn::plaintext("X")
.build(ctx, "close", hotkey(Key::Escape))
.align_right(),
]),
Menu::new(
ctx,
vec![
Choice::new("none -- no stopping or parking", Value::NoStopping),
Choice::new("both sides", Value::BothSides),
Choice::new("just on the green side", Value::RightOnly),
Choice::new("just on the blue side", Value::LeftOnly),
Choice::new(
"it changes at some point along the road",
Value::Complicated,
),
Choice::new("loading zone on one or both sides", Value::Complicated),
],
)
.named("menu"),
]))
.build(ctx),
draw: ctx.upload(batch),
osm_way_id,
data,
show,
})
}
}
impl State for ChangeWay {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();
match self.composite.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"close" => Transition::Pop,
_ => {
let value = self
.composite
.menu::<Value>("menu")
.current_choice()
.clone();
if value == Value::Complicated {
Transition::Replace(msg(
"Complicated road",
vec![
"You'll have to manually split the way in ID or JOSM and apply \
the appropriate parking tags to each section.",
],
))
} else {
self.data.insert(self.osm_way_id, value);
Transition::PopThenReplace(ParkingMapper::make(
ctx,
app,
self.show,
self.data.clone(),
))
}
}
},
_ => {
if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
return Transition::Pop;
}
Transition::Keep
}
}
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
g.redraw(&self.draw);
self.composite.draw(g);
}
}
#[cfg(target_arch = "wasm32")]
fn generate_osmc(_: &BTreeMap<i64, Value>, _: bool, _: &mut Timer) -> Result<(), Box<dyn Error>> {
Err("Woops, mapping mode isn't supported on the web yet"

View File

@ -1,6 +1,6 @@
use crate::app::{App, ShowEverything};
use crate::common::CommonState;
use crate::game::{ChooseSomething, DrawBaselayer, State, Transition, WizardState};
use crate::game::{ChooseSomething, DrawBaselayer, PromptInput, State, Transition};
use crate::render::DrawOptions;
use ezgui::{
hotkey, lctrl, Btn, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
@ -180,16 +180,19 @@ impl State for StoryMapEditor {
}
"save" => {
if self.story.name == "new story" {
return Transition::Push(WizardState::new(Box::new(|wiz, ctx, _| {
let name = wiz.wrap(ctx).input_string("Name this story map")?;
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
editor.story.name = name;
editor.story.save(app);
editor.dirty = false;
editor.redo_panel(ctx);
})))
})));
return Transition::Push(PromptInput::new(
ctx,
"Name this story map",
Box::new(|name, _, _| {
Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<StoryMapEditor>().unwrap();
editor.story.name = name;
editor.story.save(app);
editor.dirty = false;
editor.redo_panel(ctx);
}))
}),
));
} else {
self.story.save(app);
self.dirty = false;

View File

@ -1,15 +1,15 @@
use crate::app::{App, ShowEverything};
use crate::common::CommonState;
use crate::edit::{apply_map_edits, check_sidewalk_connectivity, StopSignEditor};
use crate::game::{msg, ChooseSomething, DrawBaselayer, State, Transition, WizardState};
use crate::game::{msg, ChooseSomething, DrawBaselayer, State, Transition};
use crate::render::{
draw_signal_phase, make_signal_diagram, DrawOptions, DrawTurnGroup, BIG_ARROW_THICKNESS,
};
use crate::sandbox::{spawn_agents_around, GameplayMode, SpeedControls, TimePanel};
use abstutil::Timer;
use ezgui::{
hotkey, lctrl, Btn, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, RewriteColor, Text, TextExt, UpdateType,
hotkey, lctrl, Btn, Checkbox, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, RewriteColor, Spinner, Text, TextExt, UpdateType,
VerticalAlignment, Widget,
};
use geom::{ArrowCap, Distance, Duration, Polygon};
@ -108,12 +108,24 @@ impl State for TrafficSignalEditor {
Outcome::Clicked(x) => match x.as_ref() {
"Edit entire signal" => {
return Transition::Push(edit_entire_signal(
ctx,
app,
self.i,
self.mode.clone(),
self.command_stack.get(0).cloned(),
));
}
"Change signal offset" => {
let mut new_signal = orig_signal.clone();
new_signal.offset = Duration::seconds(self.composite.spinner("offset") as f64);
self.command_stack.push(orig_signal.clone());
self.redo_stack.clear();
self.top_panel = make_top_panel(ctx, app, true, false);
app.primary.map.incremental_edit_traffic_signal(new_signal);
self.change_phase(self.current_phase, ctx, app);
return Transition::Keep;
}
"Add new phase" => {
let mut new_signal = orig_signal.clone();
new_signal.phases.push(Phase::new());
@ -128,7 +140,7 @@ impl State for TrafficSignalEditor {
x => {
if let Some(x) = x.strip_prefix("change duration of phase ") {
let idx = x.parse::<usize>().unwrap() - 1;
return Transition::Push(change_duration(app, self.i, idx));
return Transition::Push(ChangeDuration::new(ctx, app, self.i, idx));
}
if let Some(x) = x.strip_prefix("delete phase ") {
let idx = x.parse::<usize>().unwrap() - 1;
@ -469,6 +481,7 @@ pub fn make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: b
}
fn edit_entire_signal(
ctx: &mut EventCtx,
app: &App,
i: IntersectionID,
mode: GameplayMode,
@ -479,64 +492,62 @@ fn edit_entire_signal(
.map
.get_turns_in_intersection(i)
.any(|t| t.between_sidewalks());
let current_offset = app.primary.map.get_traffic_signal(i).offset;
WizardState::new(Box::new(move |wiz, ctx, app| {
let use_template = "use template";
let all_walk = "add an all-walk phase at the end";
let stop_sign = "convert to stop signs";
let close = "close intersection for construction";
let offset = "edit signal offset";
let reset = "reset to default";
let use_template = "use template";
let all_walk = "add an all-walk phase at the end";
let stop_sign = "convert to stop signs";
let close = "close intersection for construction";
let reset = "reset to default";
let mut choices = vec![use_template];
if has_sidewalks {
choices.push(all_walk);
}
// TODO Conflating stop signs and construction here
if mode.can_edit_stop_signs() {
choices.push(stop_sign);
choices.push(close);
}
choices.push(offset);
choices.push(reset);
let mut choices = vec![use_template];
if has_sidewalks {
choices.push(all_walk);
}
// TODO Conflating stop signs and construction here
if mode.can_edit_stop_signs() {
choices.push(stop_sign);
choices.push(close);
}
choices.push(reset);
let mut wizard = wiz.wrap(ctx);
match wizard.choose_string("", move || choices.clone())?.as_str() {
x if x == use_template => {
let (_, new_signal) =
wizard.choose("Use which preset for this intersection?", || {
Choice::from(ControlTrafficSignal::get_possible_policies(
&app.primary.map,
i,
&mut Timer::throwaway(),
))
})?;
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
editor
.command_stack
.push(app.primary.map.get_traffic_signal(editor.i).clone());
editor.redo_stack.clear();
editor.top_panel = make_top_panel(ctx, app, true, false);
app.primary.map.incremental_edit_traffic_signal(new_signal);
editor.change_phase(0, ctx, app);
})))
}
x if x == all_walk => {
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
let mut new_signal = orig_signal.clone();
if new_signal.convert_to_ped_scramble() {
editor.command_stack.push(orig_signal.clone());
ChooseSomething::new(
ctx,
"What do you want to change?",
Choice::strings(choices),
Box::new(move |x, ctx, app| match x.as_str() {
x if x == use_template => Transition::Replace(ChooseSomething::new(
ctx,
"Use which preset for this intersection?",
Choice::from(ControlTrafficSignal::get_possible_policies(
&app.primary.map,
i,
&mut Timer::throwaway(),
)),
Box::new(move |new_signal, _, _| {
Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
editor
.command_stack
.push(app.primary.map.get_traffic_signal(editor.i).clone());
editor.redo_stack.clear();
editor.top_panel = make_top_panel(ctx, app, true, false);
app.primary.map.incremental_edit_traffic_signal(new_signal);
editor.change_phase(0, ctx, app);
}
})))
}
}))
}),
)),
x if x == all_walk => Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
let mut new_signal = orig_signal.clone();
if new_signal.convert_to_ped_scramble() {
editor.command_stack.push(orig_signal.clone());
editor.redo_stack.clear();
editor.top_panel = make_top_panel(ctx, app, true, false);
app.primary.map.incremental_edit_traffic_signal(new_signal);
editor.change_phase(0, ctx, app);
}
})),
x if x == stop_sign => {
// First restore the original signal
if let Some(ref orig) = orig_signal {
@ -552,12 +563,7 @@ fn edit_entire_signal(
new: EditIntersection::StopSign(ControlStopSign::new(&app.primary.map, i)),
});
apply_map_edits(ctx, app, edits);
Some(Transition::PopThenReplace(Box::new(StopSignEditor::new(
ctx,
app,
i,
mode.clone(),
))))
Transition::PopThenReplace(Box::new(StopSignEditor::new(ctx, app, i, mode.clone())))
}
x if x == close => {
// First restore the original signal
@ -573,33 +579,17 @@ fn edit_entire_signal(
new: EditIntersection::Closed,
};
if let Some(err) = check_sidewalk_connectivity(ctx, app, cmd.clone()) {
Some(Transition::Replace(err))
Transition::Replace(err)
} else {
let mut edits = app.primary.map.get_edits().clone();
edits.commands.push(cmd);
apply_map_edits(ctx, app, edits);
Some(Transition::PopTwice)
Transition::PopTwice
}
}
x if x == offset => {
let new_duration = wizard.input_usize_prefilled(
"What should the offset of this traffic signal be (seconds)?",
format!("{}", current_offset.inner_seconds() as usize),
)?;
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
let mut signal = app.primary.map.get_traffic_signal(editor.i).clone();
editor.command_stack.push(signal.clone());
editor.redo_stack.clear();
editor.top_panel = make_top_panel(ctx, app, true, false);
signal.offset = Duration::seconds(new_duration as f64);
app.primary.map.incremental_edit_traffic_signal(signal);
editor.change_phase(editor.current_phase, ctx, app);
})))
}
x if x == reset => {
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
let new_signal = ControlTrafficSignal::get_possible_policies(
@ -617,59 +607,109 @@ fn edit_entire_signal(
editor.current_phase = 0;
editor.composite =
make_signal_diagram(ctx, app, editor.i, editor.current_phase, true);
})))
}))
}
_ => unreachable!(),
}
}))
}),
)
}
fn change_duration(app: &App, i: IntersectionID, idx: usize) -> Box<dyn State> {
let current_type = app.primary.map.get_traffic_signal(i).phases[idx]
.phase_type
.clone();
struct ChangeDuration {
composite: Composite,
idx: usize,
}
// TODO This UI shouldn't be a wizard
WizardState::new(Box::new(move |wiz, ctx, _| {
let mut wizard = wiz.wrap(ctx);
let new_duration = Duration::seconds(wizard.input_something(
"How long should this phase be (seconds)?",
Some(format!(
"{}",
current_type.simple_duration().inner_seconds() as usize
)),
Box::new(|line| {
line.parse::<usize>()
.ok()
.and_then(|n| if n != 0 { Some(n) } else { None })
}),
)? as f64);
let fixed = format!("Fixed: always {}", new_duration);
let adaptive = format!(
"Adaptive: some multiple of {}, based on current demand",
new_duration
);
let choice = wizard.choose_string("How should this phase be timed?", move || {
vec![fixed.clone(), adaptive.clone()]
})?;
let new_type = if choice.starts_with("Fixed") {
PhaseType::Fixed(new_duration)
} else {
PhaseType::Adaptive(new_duration)
};
Some(Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
impl ChangeDuration {
fn new(ctx: &mut EventCtx, app: &App, i: IntersectionID, idx: usize) -> Box<dyn State> {
let current = app.primary.map.get_traffic_signal(i).phases[idx]
.phase_type
.clone();
let mut new_signal = orig_signal.clone();
new_signal.phases[idx].phase_type = new_type;
editor.command_stack.push(orig_signal.clone());
editor.redo_stack.clear();
editor.top_panel = make_top_panel(ctx, app, true, false);
app.primary.map.incremental_edit_traffic_signal(new_signal);
editor.change_phase(idx, ctx, app);
})))
}))
Box::new(ChangeDuration {
composite: Composite::new(Widget::col(vec![
Widget::row(vec![
Line("How long should this phase last?")
.small_heading()
.draw(ctx),
Btn::plaintext("X")
.build(ctx, "close", hotkey(Key::Escape))
.align_right(),
]),
Widget::row(vec![
"Seconds:".draw_text(ctx),
Spinner::new(
ctx,
(5, 300),
current.simple_duration().inner_seconds() as isize,
)
.named("duration"),
]),
Widget::row(vec![
"Type:".draw_text(ctx),
Checkbox::toggle(
ctx,
"phase type",
"fixed",
"adaptive",
None,
match current {
PhaseType::Fixed(_) => true,
PhaseType::Adaptive(_) => false,
},
),
]),
Btn::text_bg2("Apply").build_def(ctx, hotkey(Key::Enter)),
]))
.build(ctx),
idx,
})
}
}
impl State for ChangeDuration {
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
match self.composite.event(ctx) {
Outcome::Clicked(x) => match x.as_ref() {
"close" => Transition::Pop,
"Apply" => {
let dt = Duration::seconds(self.composite.spinner("duration") as f64);
let new_type = if self.composite.is_checked("phase type") {
PhaseType::Fixed(dt)
} else {
PhaseType::Adaptive(dt)
};
let idx = self.idx;
return Transition::PopWithData(Box::new(move |state, ctx, app| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
let mut new_signal = orig_signal.clone();
new_signal.phases[idx].phase_type = new_type;
editor.command_stack.push(orig_signal.clone());
editor.redo_stack.clear();
editor.top_panel = make_top_panel(ctx, app, true, false);
app.primary.map.incremental_edit_traffic_signal(new_signal);
editor.change_phase(idx, ctx, app);
}));
}
_ => unreachable!(),
},
_ => {
if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
return Transition::Pop;
}
Transition::Keep
}
}
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::PreviousState
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.composite.draw(g);
}
}
fn check_for_missing_groups(

View File

@ -439,3 +439,61 @@ impl<T: 'static + Clone> State for ChooseSomething<T> {
self.composite.draw(g);
}
}
pub struct PromptInput {
composite: Composite,
cb: Box<dyn Fn(String, &mut EventCtx, &mut App) -> Transition>,
}
impl PromptInput {
pub fn new(
ctx: &mut EventCtx,
query: &str,
cb: Box<dyn Fn(String, &mut EventCtx, &mut App) -> Transition>,
) -> Box<dyn State> {
Box::new(PromptInput {
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(),
]),
Widget::text_entry(ctx, String::new(), true).named("input"),
Btn::text_fg("confirm").build_def(ctx, hotkey(Key::Enter)),
]))
.build(ctx),
cb,
})
}
}
impl State for PromptInput {
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,
"confirm" => {
let data = self.composite.text_box("input");
(self.cb)(data, ctx, app)
}
_ => unreachable!(),
},
_ => {
if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
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);
}
}

View File

@ -4,7 +4,7 @@ use crate::render::intersection::make_crosswalk;
use crate::render::{DrawTurnGroup, BIG_ARROW_THICKNESS};
use ezgui::{
hotkey, Btn, Color, Composite, EventCtx, GeomBatch, HorizontalAlignment, Key, Line, Prerender,
RewriteColor, Text, TextExt, VerticalAlignment, Widget,
RewriteColor, Spinner, Text, TextExt, VerticalAlignment, Widget,
};
use geom::{Angle, ArrowCap, Circle, Distance, Duration, Line, PolyLine, Polygon, Pt2D};
use map_model::{IntersectionID, Phase, PhaseType, TurnPriority, SIDEWALK_THICKNESS};
@ -455,6 +455,10 @@ pub fn make_signal_diagram(
);
col.push(Btn::text_fg("Add new phase").build_def(ctx, None));
col.push(Widget::row(vec![
Spinner::new(ctx, (0, 300), signal.offset.inner_seconds() as isize).named("offset"),
Btn::text_fg("Change signal offset").build_def(ctx, None),
]));
}
Composite::new(Widget::col(col))

View File

@ -1,7 +1,7 @@
use crate::app::App;
use crate::common::CityPicker;
use crate::edit::EditMode;
use crate::game::{msg, State, Transition, WizardState};
use crate::game::{msg, State, Transition};
use crate::helpers::{checkbox_per_mode, nice_map_name};
use crate::sandbox::gameplay::freeform::make_change_traffic;
use crate::sandbox::gameplay::{GameplayMode, GameplayState};
@ -191,9 +191,14 @@ impl EditScenarioModifiers {
.outline(2.0, Color::WHITE),
);
}
rows.push(Btn::text_bg2("Change trip mode").build_def(ctx, None));
rows.push(Widget::row(vec![
Btn::text_bg2("New modification").build_def(ctx, None),
Btn::text_bg2("Change trip mode").build_def(ctx, None),
Spinner::new(ctx, (2, 14), 2).named("repeat_days"),
Btn::text_bg2("Repeat schedule multiple days").build_def(ctx, None),
]));
rows.push(Widget::row(vec![
Spinner::new(ctx, (1, 100), 1).named("cancel_pct"),
Btn::text_bg2("Cancel all trips for some percent of people").build_def(ctx, None),
]));
rows.push(
Widget::row(vec![
@ -231,12 +236,6 @@ impl State for EditScenarioModifiers {
),
)));
}
"New modification" => {
return Transition::Push(new_modifier(
self.scenario_name.clone(),
self.modifiers.clone(),
));
}
"Change trip mode" => {
return Transition::Push(ChangeMode::new(
ctx,
@ -245,6 +244,26 @@ impl State for EditScenarioModifiers {
self.modifiers.clone(),
));
}
"Repeat schedule multiple days" => {
self.modifiers.push(ScenarioModifier::RepeatDays(
self.composite.spinner("repeat_days") as usize,
));
return Transition::Replace(EditScenarioModifiers::new(
ctx,
self.scenario_name.clone(),
self.modifiers.clone(),
));
}
"Cancel all trips for some percent of people" => {
self.modifiers.push(ScenarioModifier::CancelPeople(
self.composite.spinner("cancel_pct") as usize,
));
return Transition::Replace(EditScenarioModifiers::new(
ctx,
self.scenario_name.clone(),
self.modifiers.clone(),
));
}
x => {
if let Some(x) = x.strip_prefix("delete modifier ") {
self.modifiers.remove(x.parse::<usize>().unwrap() - 1);
@ -270,34 +289,6 @@ impl State for EditScenarioModifiers {
}
}
// TODO Wizard isn't the right UI for this
fn new_modifier(scenario_name: String, modifiers: Vec<ScenarioModifier>) -> Box<dyn State> {
WizardState::new(Box::new(move |wiz, ctx, _| {
let mut wizard = wiz.wrap(ctx);
let new_mod = match wizard
.choose_string("", || {
vec!["repeat days", "cancel all trips for some people"]
})?
.as_str()
{
x if x == "repeat days" => ScenarioModifier::RepeatDays(
wizard.input_usize("Repeat everyone's schedule how many days?")?,
),
x if x == "cancel all trips for some people" => ScenarioModifier::CancelPeople(
wizard.input_percent("What percent of people should cancel trips? (0 to 100)")?,
),
_ => unreachable!(),
};
let mut mods = modifiers.clone();
mods.push(new_mod);
Some(Transition::PopThenReplace(EditScenarioModifiers::new(
ctx,
scenario_name.clone(),
mods,
)))
}))
}
struct ChangeMode {
composite: Composite,
scenario_name: String,