diff --git a/ezgui/src/lib.rs b/ezgui/src/lib.rs index 71991f5250..659ce76cc7 100644 --- a/ezgui/src/lib.rs +++ b/ezgui/src/lib.rs @@ -20,8 +20,8 @@ pub use crate::runner::{run, EventLoopMode, GUI}; pub use crate::screen_geom::{ScreenDims, ScreenPt, ScreenRectangle}; pub use crate::text::{Line, Text, TextSpan, HOTKEY_COLOR}; pub use crate::widgets::{ - Autocomplete, Choice, ItemSlider, ModalMenu, Scroller, Slider, SliderWithTextBox, Warper, - WarpingItemSlider, Wizard, WrappedWizard, + Autocomplete, Choice, ItemSlider, ModalMenu, Scroller, SidebarPos, Slider, SliderWithTextBox, + Warper, WarpingItemSlider, Wizard, WrappedWizard, }; pub enum InputResult { diff --git a/ezgui/src/widgets/mod.rs b/ezgui/src/widgets/mod.rs index 8e8a2d45a0..d37d5f0fca 100644 --- a/ezgui/src/widgets/mod.rs +++ b/ezgui/src/widgets/mod.rs @@ -11,7 +11,7 @@ mod wizard; pub use self::autocomplete::Autocomplete; pub use self::menu::{Menu, Position}; -pub use self::modal_menu::ModalMenu; +pub use self::modal_menu::{ModalMenu, SidebarPos}; pub(crate) use self::screenshot::{screenshot_current, screenshot_everything}; pub use self::scroller::Scroller; pub use self::slider::{ItemSlider, Slider, SliderWithTextBox, WarpingItemSlider}; diff --git a/ezgui/src/widgets/modal_menu.rs b/ezgui/src/widgets/modal_menu.rs index 7744acea8f..efe941d3ee 100644 --- a/ezgui/src/widgets/modal_menu.rs +++ b/ezgui/src/widgets/modal_menu.rs @@ -1,10 +1,18 @@ use crate::widgets::{Menu, Position}; -use crate::{EventCtx, GfxCtx, InputResult, MultiKey, Text}; +use crate::{EventCtx, GfxCtx, InputResult, MultiKey, ScreenPt, Slider, Text}; + +#[derive(Clone, Copy)] +pub enum SidebarPos { + Left, + Right, + At(ScreenPt), +} pub struct ModalMenu { menu: Menu<()>, chosen_action: Option, choice_groups: Vec, String, ())>>, + pos: SidebarPos, } impl ModalMenu { @@ -22,12 +30,13 @@ impl ModalMenu { .collect() }) .collect(); + let pos = SidebarPos::Right; let mut menu = Menu::new( Text::prompt(prompt_line), choice_groups.clone(), false, true, - Position::TopRightOfScreen, + pos.pos(), ctx.canvas, ); menu.mark_all_inactive(); @@ -38,9 +47,21 @@ impl ModalMenu { menu, chosen_action: None, choice_groups, + pos, } } + pub fn set_prompt(mut self, ctx: &mut EventCtx, prompt: Text) -> ModalMenu { + self.menu.change_prompt(prompt, ctx.canvas); + self + } + + pub fn set_pos(mut self, ctx: &mut EventCtx, pos: SidebarPos) -> ModalMenu { + self.pos = pos; + self.rebuild_menu(ctx); + self + } + pub fn handle_event(&mut self, ctx: &mut EventCtx, new_prompt: Option) { if let Some(ref action) = self.chosen_action { panic!("Caller didn't consume modal action '{}'", action); @@ -63,11 +84,6 @@ impl ModalMenu { } } - pub fn set_prompt(mut self, ctx: &mut EventCtx, prompt: Text) -> ModalMenu { - self.menu.change_prompt(prompt, ctx.canvas); - self - } - pub fn add_action(&mut self, key: Option, name: &str, ctx: &mut EventCtx) { self.choice_groups .last_mut() @@ -115,7 +131,7 @@ impl ModalMenu { self.choice_groups.clone(), false, true, - Position::TopRightOfScreen, + self.pos.pos(), ctx.canvas, ); menu.mark_all_inactive(); @@ -127,3 +143,18 @@ impl ModalMenu { self.menu = menu; } } + +impl SidebarPos { + fn pos(&self) -> Position { + match self { + SidebarPos::Left => Position::SomeCornerAt(ScreenPt::new(0.0, 0.0)), + SidebarPos::Right => Position::TopRightOfScreen, + SidebarPos::At(pt) => Position::SomeCornerAt(*pt), + } + } + + // TODO Assumes the slider never moves + pub fn below(slider: &Slider) -> SidebarPos { + SidebarPos::At(slider.below_top_left()) + } +} diff --git a/ezgui/src/widgets/slider.rs b/ezgui/src/widgets/slider.rs index 02e3c65f43..d68e1e7bb5 100644 --- a/ezgui/src/widgets/slider.rs +++ b/ezgui/src/widgets/slider.rs @@ -15,17 +15,16 @@ const HORIZ_PADDING: f64 = 60.0; const VERT_PADDING: f64 = 20.0; pub struct Slider { - top_left: ScreenPt, + pub top_left: ScreenPt, current_percent: f64, mouse_on_slider: bool, dragging: bool, } impl Slider { - // TODO Easier placement options. - pub fn new(top_left_at: Option) -> Slider { + pub fn new(top_left: ScreenPt) -> Slider { Slider { - top_left: top_left_at.unwrap_or_else(|| ScreenPt::new(0.0, 0.0)), + top_left, current_percent: 0.0, mouse_on_slider: false, dragging: false, @@ -99,7 +98,7 @@ impl Slider { false } - pub fn draw(&self, g: &mut GfxCtx, label: Option) { + pub fn draw(&self, g: &mut GfxCtx) { g.fork_screenspace(); // A nice background for the entire thing @@ -155,16 +154,13 @@ impl Slider { }, &self.slider_geom(), ); + } - if let Some(ref txt) = label { - g.draw_text_at_screenspace_topleft( - txt, - ScreenPt::new( - self.top_left.x, - self.top_left.y + BAR_HEIGHT + 2.0 * VERT_PADDING, - ), - ); - } + pub fn below_top_left(&self) -> ScreenPt { + ScreenPt::new( + self.top_left.x, + self.top_left.y + BAR_HEIGHT + 2.0 * VERT_PADDING, + ) } fn slider_geom(&self) -> Polygon { @@ -216,7 +212,7 @@ impl ItemSlider { ItemSlider { items, - slider: Slider::new(None), + slider: Slider::new(ScreenPt::new(0.0, 0.0)), menu: ModalMenu::new(menu_title, choices, ctx), noun: noun.to_string(), @@ -249,6 +245,7 @@ impl ItemSlider { pub fn draw(&self, g: &mut GfxCtx) { self.menu.draw(g); + self.slider.draw(g); let idx = self.slider.get_value(self.items.len()); let mut txt = Text::from(Line(format!( @@ -258,7 +255,7 @@ impl ItemSlider { abstutil::prettyprint_usize(self.items.len()) ))); txt.extend(&self.items[idx].1); - self.slider.draw(g, Some(txt)); + g.draw_text_at_screenspace_topleft(&txt, self.slider.below_top_left()); } pub fn get(&self) -> (usize, &T) { @@ -358,7 +355,7 @@ impl SliderWithTextBox { top_left.y -= (BAR_HEIGHT + 2.0 * VERT_PADDING + LINE_HEIGHT) / 2.0; SliderWithTextBox { - slider: Slider::new(Some(top_left)), + slider: Slider::new(top_left), tb: TextBox::new(prompt, None), low, high, @@ -401,6 +398,7 @@ impl SliderWithTextBox { } pub fn draw(&self, g: &mut GfxCtx) { - self.slider.draw(g, Some(self.tb.get_text())); + self.slider.draw(g); + g.draw_text_at_screenspace_topleft(&self.tb.get_text(), self.slider.below_top_left()); } } diff --git a/game/src/abtest/mod.rs b/game/src/abtest/mod.rs index b26c77e731..73675d9897 100644 --- a/game/src/abtest/mod.rs +++ b/game/src/abtest/mod.rs @@ -9,7 +9,8 @@ use crate::game::{State, Transition}; use crate::render::MIN_ZOOM_FOR_DETAIL; use crate::ui::{PerMapUI, UI}; use ezgui::{ - hotkey, lctrl, Color, EventCtx, EventLoopMode, GeomBatch, GfxCtx, Key, Line, ModalMenu, Text, + hotkey, lctrl, Color, EventCtx, EventLoopMode, GeomBatch, GfxCtx, Key, Line, ModalMenu, + ScreenPt, Text, }; use geom::{Circle, Distance, Line, PolyLine}; use map_model::{Map, LANE_THICKNESS}; @@ -37,9 +38,6 @@ impl ABTestMode { "A/B Test Mode", vec![ vec![ - (hotkey(Key::LeftBracket), "slow down"), - (hotkey(Key::RightBracket), "speed up"), - (hotkey(Key::Space), "pause/resume"), (hotkey(Key::M), "step forwards 0.1s"), (hotkey(Key::N), "step forwards 10 mins"), (hotkey(Key::B), "jump to specific time"), @@ -64,7 +62,7 @@ impl ABTestMode { ], ctx, ), - speed: SpeedControls::new(ctx, None), + speed: SpeedControls::new(ctx, ScreenPt::new(0.0, 0.0)), primary_agent_tools: AgentTools::new(), secondary_agent_tools: AgentTools::new(), diff_trip: None, @@ -191,7 +189,7 @@ impl State for ABTestMode { } } - if let Some(dt) = self.speed.event(ctx, &mut self.menu, ui.primary.sim.time()) { + if let Some(dt) = self.speed.event(ctx, ui.primary.sim.time()) { ui.primary.sim.step(&ui.primary.map, dt); { let s = ui.secondary.as_mut().unwrap(); diff --git a/game/src/common/speed.rs b/game/src/common/speed.rs index 102d9596ba..c61e816851 100644 --- a/game/src/common/speed.rs +++ b/game/src/common/speed.rs @@ -1,5 +1,5 @@ use abstutil::elapsed_seconds; -use ezgui::{EventCtx, GfxCtx, Line, ModalMenu, ScreenPt, Slider, Text}; +use ezgui::{hotkey, EventCtx, GfxCtx, Key, Line, ModalMenu, ScreenPt, SidebarPos, Slider, Text}; use geom::Duration; use std::time::Instant; @@ -9,6 +9,7 @@ const SPEED_CAP: f64 = 10.0 * 60.0; pub struct SpeedControls { slider: Slider, + menu: ModalMenu, state: State, } @@ -23,27 +24,54 @@ enum State { } impl SpeedControls { - pub fn new(ctx: &mut EventCtx, top_left_at: Option) -> SpeedControls { + pub fn new(ctx: &mut EventCtx, top_left_at: ScreenPt) -> SpeedControls { let mut slider = Slider::new(top_left_at); slider.set_percent(ctx, 1.0 / SPEED_CAP); + + let menu = ModalMenu::new( + "Speed", + vec![vec![ + (hotkey(Key::LeftBracket), "slow down"), + (hotkey(Key::RightBracket), "speed up"), + (hotkey(Key::Space), "pause/resume"), + ]], + ctx, + ) + .set_pos(ctx, SidebarPos::below(&slider)); + SpeedControls { slider, + menu, state: State::Paused, } } // Returns the amount of simulation time to step, if running. - pub fn event( - &mut self, - ctx: &mut EventCtx, - menu: &mut ModalMenu, - current_sim_time: Duration, - ) -> Option { + pub fn event(&mut self, ctx: &mut EventCtx, current_sim_time: Duration) -> Option { + let mut txt = Text::prompt("Speed"); + if let State::Running { + ref speed_description, + .. + } = self.state + { + txt.add(Line(format!( + "{} / desired {:.2}x", + speed_description, + self.desired_speed() + ))); + } else { + txt.add(Line(format!( + "paused / desired {:.2}x", + self.desired_speed() + ))); + } + self.menu.handle_event(ctx, Some(txt)); + let desired_speed = self.desired_speed(); - if desired_speed != SPEED_CAP && menu.action("speed up") { + if desired_speed != SPEED_CAP && self.menu.action("speed up") { self.slider .set_percent(ctx, ((desired_speed + ADJUST_SPEED) / SPEED_CAP).min(1.0)); - } else if desired_speed != 0.0 && menu.action("slow down") { + } else if desired_speed != 0.0 && self.menu.action("slow down") { self.slider .set_percent(ctx, ((desired_speed - ADJUST_SPEED) / SPEED_CAP).max(0.0)); } else if self.slider.event(ctx) { @@ -52,7 +80,7 @@ impl SpeedControls { match self.state { State::Paused => { - if menu.action("pause/resume") { + if self.menu.action("pause/resume") { let now = Instant::now(); self.state = State::Running { last_step: now, @@ -70,7 +98,7 @@ impl SpeedControls { ref mut last_measurement, ref mut last_measurement_sim, } => { - if menu.action("pause/resume") { + if self.menu.action("pause/resume") { self.state = State::Paused; } else if ctx.input.nonblocking_is_update_event() { ctx.input.use_update_event(); @@ -94,24 +122,8 @@ impl SpeedControls { } pub fn draw(&self, g: &mut GfxCtx) { - let mut txt = Text::new(); - if let State::Running { - ref speed_description, - .. - } = self.state - { - txt.add(Line(format!( - "Speed: {} / desired {:.2}x", - speed_description, - self.desired_speed() - ))); - } else { - txt.add(Line(format!( - "Speed: paused / desired {:.2}x", - self.desired_speed() - ))); - } - self.slider.draw(g, Some(txt)); + self.slider.draw(g); + self.menu.draw(g); } pub fn pause(&mut self) { diff --git a/game/src/mission/all_trips.rs b/game/src/mission/all_trips.rs index 65e30767ec..9c1da6b01c 100644 --- a/game/src/mission/all_trips.rs +++ b/game/src/mission/all_trips.rs @@ -69,19 +69,14 @@ impl TripsVisualizer { (hotkey(Key::F), "goto start of day"), (hotkey(Key::L), "goto end of day"), ], - vec![ - (hotkey(Key::LeftBracket), "slow down"), - (hotkey(Key::RightBracket), "speed up"), - (hotkey(Key::Space), "pause/resume"), - ], vec![(hotkey(Key::Escape), "quit")], ], ctx, ), trips, - time_slider: Slider::new(None), + time_slider: Slider::new(ScreenPt::new(0.0, 0.0)), // TODO hardcoding placement... - speed: SpeedControls::new(ctx, Some(ScreenPt::new(500.0, 0.0))), + speed: SpeedControls::new(ctx, ScreenPt::new(500.0, 0.0)), active_trips: Vec::new(), } } @@ -132,7 +127,7 @@ impl State for TripsVisualizer { self.time_slider.set_percent(ctx, 1.0); } else if self.time_slider.event(ctx) { // Value changed, fall-through - } else if let Some(dt) = self.speed.event(ctx, &mut self.menu, time) { + } else if let Some(dt) = self.speed.event(ctx, time) { // TODO Speed description is briefly weird when we jump backwards with the other // control. self.time_slider @@ -187,8 +182,11 @@ impl State for TripsVisualizer { batch.draw(g); self.menu.draw(g); - self.time_slider - .draw(g, Some(Text::from(Line(format!("At {}", time))))); + self.time_slider.draw(g); + g.draw_text_at_screenspace_topleft( + &Text::from(Line(format!("At {}", time))), + self.time_slider.below_top_left(), + ); self.speed.draw(g); CommonState::draw_osd(g, ui, &ui.primary.current_selection); } diff --git a/game/src/sandbox/mod.rs b/game/src/sandbox/mod.rs index a946fcf4fa..3c3a60fd6b 100644 --- a/game/src/sandbox/mod.rs +++ b/game/src/sandbox/mod.rs @@ -13,7 +13,8 @@ use crate::game::{State, Transition, WizardState}; use crate::helpers::ID; use crate::ui::{ShowEverything, UI}; use ezgui::{ - hotkey, lctrl, Choice, EventCtx, EventLoopMode, GfxCtx, Key, Line, ModalMenu, Text, Wizard, + hotkey, lctrl, Choice, EventCtx, EventLoopMode, GfxCtx, Key, Line, ModalMenu, ScreenPt, Text, + Wizard, }; use geom::Duration; use sim::Sim; @@ -32,7 +33,7 @@ pub struct SandboxMode { impl SandboxMode { pub fn new(ctx: &mut EventCtx, ui: &UI) -> SandboxMode { SandboxMode { - speed: SpeedControls::new(ctx, None), + speed: SpeedControls::new(ctx, ScreenPt::new(0.0, 0.0)), agent_tools: AgentTools::new(), time_travel: time_travel::InactiveTimeTravel::new(), trip_stats: trip_stats::TripStats::new( @@ -45,9 +46,6 @@ impl SandboxMode { "Sandbox Mode", vec![ vec![ - (hotkey(Key::RightBracket), "speed up"), - (hotkey(Key::LeftBracket), "slow down"), - (hotkey(Key::Space), "pause/resume"), (hotkey(Key::M), "step forwards 0.1s"), (hotkey(Key::N), "step forwards 10 mins"), (hotkey(Key::B), "jump to specific time"), @@ -164,7 +162,7 @@ impl State for SandboxMode { } } - if let Some(dt) = self.speed.event(ctx, &mut self.menu, ui.primary.sim.time()) { + if let Some(dt) = self.speed.event(ctx, ui.primary.sim.time()) { // If speed is too high, don't be unresponsive for too long. // TODO This should probably match the ezgui framerate. ui.primary