diff --git a/ezgui/src/managed.rs b/ezgui/src/managed.rs index 5394748a95..19eeccddb1 100644 --- a/ezgui/src/managed.rs +++ b/ezgui/src/managed.rs @@ -1,10 +1,10 @@ use crate::layout::Widget; use crate::text; -use crate::widgets::{Checkbox, PopupMenu, TextBox}; +use crate::widgets::{Checkbox, Dropdown, PopupMenu, TextBox}; use crate::{ - Button, Color, Drawable, EventCtx, Filler, GeomBatch, GfxCtx, Histogram, HorizontalAlignment, - JustDraw, Line, MultiKey, Plot, RewriteColor, ScreenDims, ScreenPt, ScreenRectangle, Slider, - Text, VerticalAlignment, + Button, Choice, Color, Drawable, EventCtx, Filler, GeomBatch, GfxCtx, Histogram, + HorizontalAlignment, JustDraw, Line, MultiKey, Plot, RewriteColor, ScreenDims, ScreenPt, + ScreenRectangle, Slider, Text, VerticalAlignment, }; use abstutil::Cloneable; use geom::{Distance, Duration, Polygon}; @@ -32,6 +32,7 @@ enum WidgetType { Btn(Button), Checkbox(Checkbox), TextBox(TextBox), + Dropdown(Dropdown), Slider(String), Menu(String), Filler(String), @@ -326,6 +327,22 @@ impl ManagedWidget { ManagedWidget::new(WidgetType::TextBox(TextBox::new(ctx, 50, prefilled))).bg(text::BG_COLOR) } + pub fn dropdown( + ctx: &EventCtx, + label: &str, + default_value: T, + choices: Vec>, + ) -> ManagedWidget { + ManagedWidget::new(WidgetType::Dropdown(Dropdown::new( + ctx, + label, + default_value, + choices, + ))) + .named(label) + .outline(2.0, Color::WHITE) + } + pub(crate) fn duration_plot(plot: Plot) -> ManagedWidget { ManagedWidget::new(WidgetType::DurationPlot(plot)) } @@ -389,6 +406,9 @@ impl ManagedWidget { WidgetType::TextBox(ref mut textbox) => { textbox.event(ctx); } + WidgetType::Dropdown(ref mut dropdown) => { + dropdown.event(ctx, &self.rect); + } WidgetType::Slider(ref name) => { sliders.get_mut(name).unwrap().event(ctx); } @@ -426,6 +446,7 @@ impl ManagedWidget { WidgetType::Btn(ref btn) => btn.draw(g), WidgetType::Checkbox(ref checkbox) => checkbox.draw(g), WidgetType::TextBox(ref textbox) => textbox.draw(g), + WidgetType::Dropdown(ref dropdown) => dropdown.draw(g), WidgetType::Slider(ref name) => { if name != "horiz scrollbar" && name != "vert scrollbar" { sliders[name].draw(g); @@ -461,6 +482,7 @@ impl ManagedWidget { WidgetType::Btn(ref widget) => widget, WidgetType::Checkbox(ref widget) => widget, WidgetType::TextBox(ref widget) => widget, + WidgetType::Dropdown(ref widget) => widget, WidgetType::Slider(ref name) => &sliders[name], WidgetType::Menu(ref name) => &menus[name], WidgetType::Filler(ref name) => &fillers[name], @@ -573,6 +595,9 @@ impl ManagedWidget { WidgetType::TextBox(ref mut widget) => { widget.set_pos(top_left); } + WidgetType::Dropdown(ref mut widget) => { + widget.set_pos(top_left); + } WidgetType::Slider(ref name) => { sliders.get_mut(name).unwrap().set_pos(top_left); } @@ -636,6 +661,7 @@ impl ManagedWidget { | WidgetType::Filler(_) | WidgetType::Checkbox(_) | WidgetType::TextBox(_) + | WidgetType::Dropdown(_) | WidgetType::DurationPlot(_) | WidgetType::UsizePlot(_) => {} WidgetType::Histogram(_) => {} @@ -668,9 +694,10 @@ impl ManagedWidget { fn find(&self, name: &str) -> Option<&ManagedWidget> { let found = match self.widget { // TODO Consolidate and just do this - WidgetType::Draw(_) | WidgetType::Checkbox(_) | WidgetType::TextBox(_) => { - self.id == Some(name.to_string()) - } + WidgetType::Draw(_) + | WidgetType::Checkbox(_) + | WidgetType::TextBox(_) + | WidgetType::Dropdown(_) => self.id == Some(name.to_string()), WidgetType::Btn(ref btn) => btn.action == name, WidgetType::Slider(ref n) => n == name, WidgetType::Menu(ref n) => n == name, @@ -697,9 +724,10 @@ impl ManagedWidget { fn find_mut(&mut self, name: &str) -> Option<&mut ManagedWidget> { let found = match self.widget { // TODO Consolidate and just do this - WidgetType::Draw(_) | WidgetType::Checkbox(_) | WidgetType::TextBox(_) => { - self.id == Some(name.to_string()) - } + WidgetType::Draw(_) + | WidgetType::Checkbox(_) + | WidgetType::TextBox(_) + | WidgetType::Dropdown(_) => self.id == Some(name.to_string()), WidgetType::Btn(ref btn) => btn.action == name, WidgetType::Slider(ref n) => n == name, WidgetType::Menu(ref n) => n == name, @@ -1009,6 +1037,14 @@ impl Composite { } } + // TODO This invalidates the dropdown! + pub fn dropdown_value(&mut self, name: &str) -> T { + match self.find_mut(name).widget { + WidgetType::Dropdown(ref mut dropdown) => dropdown.take_value(), + _ => panic!("{} isn't a dropdown", name), + } + } + pub fn filler_rect(&self, name: &str) -> ScreenRectangle { let f = &self.fillers[name]; ScreenRectangle::top_left(f.top_left, f.dims) diff --git a/ezgui/src/widgets/dropdown.rs b/ezgui/src/widgets/dropdown.rs new file mode 100644 index 0000000000..4ddf3fa011 --- /dev/null +++ b/ezgui/src/widgets/dropdown.rs @@ -0,0 +1,125 @@ +use crate::layout::Widget; +use crate::widgets::PopupMenu; +use crate::{ + Button, Choice, Color, EventCtx, GfxCtx, InputResult, Line, ScreenDims, ScreenPt, + ScreenRectangle, Text, +}; +use std::any::Any; + +pub struct Dropdown { + current_idx: usize, + btn: Button, + menu: Option>, + label: String, + + choices: Vec>>, +} + +impl Dropdown { + pub fn new( + ctx: &EventCtx, + label: &str, + default_value: T, + choices: Vec>, + ) -> Dropdown { + let current_idx = choices + .iter() + .position(|c| c.data == default_value) + .unwrap(); + + Dropdown { + current_idx, + btn: make_btn(ctx, &choices[current_idx].label, label), + menu: None, + label: label.to_string(), + + choices: choices + .into_iter() + .map(|c| { + // TODO Can't use with_value here :( + let data: Box = Box::new(c.data); + Choice { + label: c.label, + data, + hotkey: c.hotkey, + active: c.active, + tooltip: c.tooltip, + } + }) + .collect(), + } + } + + pub fn event(&mut self, ctx: &mut EventCtx, our_rect: &ScreenRectangle) { + if let Some(ref mut m) = self.menu { + m.event(ctx); + match m.state { + InputResult::StillActive => {} + InputResult::Canceled => { + self.menu = None; + } + InputResult::Done(_, idx) => { + self.menu = None; + self.current_idx = idx; + let top_left = self.btn.top_left; + // TODO Recalculate layout when this happens... outline around button should + // change + self.btn = make_btn(ctx, &self.choices[self.current_idx].label, &self.label); + self.btn.set_pos(top_left); + } + } + } else { + self.btn.event(ctx); + if self.btn.clicked() { + // TODO set current idx in menu + // TODO Choice::map_value? + let mut menu = PopupMenu::new( + ctx, + self.choices + .iter() + .enumerate() + .map(|(idx, c)| c.with_value(idx)) + .collect(), + ); + menu.set_pos(ScreenPt::new(our_rect.x1, our_rect.y2 + 15.0)); + self.menu = Some(menu); + } + } + } + + pub fn draw(&self, g: &mut GfxCtx) { + self.btn.draw(g); + if let Some(ref m) = self.menu { + m.draw(g); + } + } + + // TODO This invalidates the entire widget! + pub fn take_value(&mut self) -> T { + let data: Box = self.choices.remove(self.current_idx).data; + let boxed: Box = data.downcast().unwrap(); + *boxed + } +} + +impl Widget for Dropdown { + fn get_dims(&self) -> ScreenDims { + self.btn.get_dims() + } + + fn set_pos(&mut self, top_left: ScreenPt) { + self.btn.set_pos(top_left); + } +} + +fn make_btn(ctx: &EventCtx, name: &str, label: &str) -> Button { + let txt = Text::from(Line(format!("{} ▼", name))); + Button::text_no_bg( + txt.clone(), + txt.change_fg(Color::ORANGE), + None, + label, + true, + ctx, + ) +} diff --git a/ezgui/src/widgets/mod.rs b/ezgui/src/widgets/mod.rs index 865b93dc39..dc783433a1 100644 --- a/ezgui/src/widgets/mod.rs +++ b/ezgui/src/widgets/mod.rs @@ -1,6 +1,7 @@ mod autocomplete; mod button; mod checkbox; +mod dropdown; mod filler; mod histogram; mod modal_menu; @@ -16,6 +17,7 @@ mod wizard; pub use self::autocomplete::Autocomplete; pub use self::button::Button; pub use self::checkbox::Checkbox; +pub(crate) use self::dropdown::Dropdown; pub use self::filler::Filler; pub use self::histogram::Histogram; pub use self::modal_menu::ModalMenu; diff --git a/ezgui/src/widgets/wizard.rs b/ezgui/src/widgets/wizard.rs index 3066346dfb..1598837849 100644 --- a/ezgui/src/widgets/wizard.rs +++ b/ezgui/src/widgets/wizard.rs @@ -297,13 +297,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> { self.ctx, choices .into_iter() - .map(|c| Choice { - label: c.label, - data: c.data.clone_box(), - hotkey: c.hotkey, - active: c.active, - tooltip: c.tooltip, - }) + .map(|c| c.with_value(c.data.clone_box())) .collect(), ), ) @@ -444,7 +438,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> { } } -pub struct Choice { +pub struct Choice { pub(crate) label: String, pub data: T, pub(crate) hotkey: Option, @@ -452,7 +446,7 @@ pub struct Choice { pub(crate) tooltip: Option, } -impl Choice { +impl Choice { pub fn new>(label: S, data: T) -> Choice { Choice { label: label.into(), @@ -490,4 +484,14 @@ impl Choice { self.tooltip = Some(info.into()); self } + + pub(crate) fn with_value(&self, data: X) -> Choice { + Choice { + label: self.label.clone(), + data, + hotkey: self.hotkey.clone(), + active: self.active, + tooltip: self.tooltip.clone(), + } + } } diff --git a/game/src/edit/mapping_traffic_signals.rs b/game/src/edit/mapping_traffic_signals.rs index d2712de9cf..df4ce78234 100644 --- a/game/src/edit/mapping_traffic_signals.rs +++ b/game/src/edit/mapping_traffic_signals.rs @@ -1,13 +1,12 @@ use crate::app::App; use crate::colors; -use crate::game::{State, Transition, WizardState}; +use crate::game::{State, Transition}; use crate::managed::WrappedComposite; use ezgui::{hotkey, Choice, Composite, EventCtx, GfxCtx, Key, Line, ManagedWidget, Outcome, Text}; use map_model::{ExtraMappingData, IntersectionID}; pub struct EditMetadata { composite: Composite, - walk_buttons: traffic_signals::WalkButtons, } impl EditMetadata { @@ -34,7 +33,6 @@ impl EditMetadata { }); EditMetadata { - walk_buttons: data.walk_buttons.clone(), composite: Composite::new( ManagedWidget::col(vec![ ManagedWidget::row(vec![ @@ -46,13 +44,15 @@ impl EditMetadata { ]), ManagedWidget::row(vec![ ManagedWidget::draw_text(ctx, Text::from(Line("Walk buttons: "))), - WrappedComposite::nice_text_button( + ManagedWidget::dropdown( ctx, - Text::from(Line(format!("{} ▼", describe(&data.walk_buttons),))), - None, "change walk buttons", - ) - .margin(5), + data.walk_buttons, + traffic_signals::WalkButtons::all() + .into_iter() + .map(|btn| Choice::new(describe(&btn), btn)) + .collect(), + ), ]), ManagedWidget::draw_text(ctx, Text::from(Line("The mapper").roboto_bold())), ManagedWidget::draw_text( @@ -114,36 +114,12 @@ impl State for EditMetadata { "X" => { return Transition::Pop; } - "change walk buttons" => { - return Transition::Push(WizardState::new(Box::new(|wiz, ctx, _| { - let (_, btn) = wiz.wrap(ctx).choose( - "What kind of walk buttons does this intersection have?", - || { - traffic_signals::WalkButtons::all() - .into_iter() - .map(|btn| Choice::new(describe(&btn), btn)) - .collect() - }, - )?; - Some(Transition::PopWithData(Box::new(move |state, _, ctx| { - let mut edit = state.downcast_mut::().unwrap(); - edit.composite.replace( - ctx, - "change walk buttons", - WrappedComposite::nice_text_button( - ctx, - Text::from(Line(format!("{} ▼", describe(&btn)))), - None, - "change walk buttons", - ), - ); - edit.walk_buttons = btn; - }))) - }))); - } "Done" => { let mut new_data = ExtraMappingData { - walk_buttons: self.walk_buttons.clone(), + walk_buttons: self + .composite + .dropdown_value::("change walk buttons") + .clone(), observed: traffic_signals::Metadata { author: self.composite.text_box("observed author"), datetime: self.composite.text_box("observed datetime"), diff --git a/traffic_signals/Cargo.toml b/traffic_signals/Cargo.toml index 9c9a707034..57dc6c46db 100644 --- a/traffic_signals/Cargo.toml +++ b/traffic_signals/Cargo.toml @@ -5,7 +5,6 @@ authors = ["Dustin Carlino "] edition = "2018" [dependencies] -abstutil = { path = "../abstutil" } include_dir = "0.5.0" serde = "1.0.89" serde_derive = "1.0.98" diff --git a/traffic_signals/src/lib.rs b/traffic_signals/src/lib.rs index ecb84fcf6e..0d1a2275ad 100644 --- a/traffic_signals/src/lib.rs +++ b/traffic_signals/src/lib.rs @@ -120,9 +120,6 @@ impl WalkButtons { } } -// TODO Temporary -impl abstutil::Cloneable for WalkButtons {} - static DATA: include_dir::Dir = include_dir::include_dir!("src/data"); /// Returns all traffic signal data compiled into this build, keyed by OSM node ID.