diff --git a/docs/TODO_ux.md b/docs/TODO_ux.md index 5708568bfa..6f7e6d4104 100644 --- a/docs/TODO_ux.md +++ b/docs/TODO_ux.md @@ -19,18 +19,16 @@ ## General ezgui stuff - optionally limit canvas scrolling/zooming to some map bounds -- when dragging, dont give mouse movement to UI elements - start context menu when left click releases and we're not dragging -- can we change labels in modal or top menu? show/hide - distinguish hints from status of modal menus, for hiding purposes - move context menus out of ezgui - simplify/remove UserInput. - maybe separate impls for context, wizard, modal menu make sense. +- arbitrary viewports?! +- tiling wm ## New features -- swap direction of one-way -- convert between one- and two-way if there's enough space - collapse smaller roads/neighborhoods and just show aggregate stats about them (in/out flow, moving/blocked within) - undo support for edits diff --git a/ezgui/src/drawing.rs b/ezgui/src/drawing.rs index 3178523491..e096cb3d6d 100644 --- a/ezgui/src/drawing.rs +++ b/ezgui/src/drawing.rs @@ -344,14 +344,22 @@ impl GeomBatch { self.list.extend(other.list.clone()); } + pub fn consume(self) -> Vec<(Color, Polygon)> { + self.list + } + pub fn draw(self, g: &mut GfxCtx) { let refs = self.list.iter().map(|(color, p)| (*color, p)).collect(); let obj = g.prerender.upload_temporary(refs); g.redraw(&obj); } - pub(crate) fn members(&self) -> &Vec<(Color, Polygon)> { - &self.list + pub(crate) fn get_dims(&self) -> ScreenDims { + let mut bounds = Bounds::new(); + for (_, poly) in &self.list { + bounds.union(poly.get_bounds()); + } + ScreenDims::new(bounds.max_x - bounds.min_x, bounds.max_y - bounds.min_y) } } @@ -469,7 +477,7 @@ impl<'a> Prerender<'a> { } pub struct MultiText { - list: Vec<(Text, ScreenPt)>, + pub(crate) list: Vec<(Text, ScreenPt)>, } impl MultiText { diff --git a/ezgui/src/lib.rs b/ezgui/src/lib.rs index a2e70866f6..b916eeb5cc 100644 --- a/ezgui/src/lib.rs +++ b/ezgui/src/lib.rs @@ -21,8 +21,8 @@ pub use crate::runner::{run, EventLoopMode, Settings, 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, NewScroller, Scroller, Slider, SliderWithTextBox, + Warper, WarpingItemSlider, Wizard, WrappedWizard, }; pub enum InputResult { diff --git a/ezgui/src/widgets/button.rs b/ezgui/src/widgets/button.rs index 893ba732f5..db454fc728 100644 --- a/ezgui/src/widgets/button.rs +++ b/ezgui/src/widgets/button.rs @@ -1,6 +1,6 @@ use crate::layout::Widget; use crate::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, ScreenDims, ScreenPt, ScreenRectangle}; -use geom::{Bounds, Circle, Distance, Polygon, Pt2D}; +use geom::{Circle, Distance, Polygon, Pt2D}; // TODO Tooltips? // TODO Hotkeys? @@ -17,8 +17,8 @@ pub struct Button { impl Button { // Top-left should be at Pt2D::new(0.0, 0.0). Must have same dimensions. pub fn new(normal: GeomBatch, hovered: GeomBatch, ctx: &EventCtx) -> Button { - let dims = geom_to_dims(&normal); - assert_eq!(dims, geom_to_dims(&hovered)); + let dims = normal.get_dims(); + assert_eq!(dims, hovered.get_dims()); Button { draw_normal: ctx.prerender.upload(normal), draw_hovered: ctx.prerender.upload(hovered), @@ -80,16 +80,6 @@ impl Widget for Button { } } -fn geom_to_dims(batch: &GeomBatch) -> ScreenDims { - let mut bounds = Bounds::new(); - for (_, poly) in batch.members() { - bounds.union(poly.get_bounds()); - } - assert!(bounds.min_x >= 0.0); - assert!(bounds.min_y >= 0.0); - ScreenDims::new(bounds.max_x, bounds.max_y) -} - const ICON_BACKGROUND: Color = Color::grey(0.5); const ICON_BACKGROUND_SELECTED: Color = Color::YELLOW; const ICON_SYMBOL: Color = Color::grey(0.8); diff --git a/ezgui/src/widgets/mod.rs b/ezgui/src/widgets/mod.rs index 62ee99eace..02c10e78f9 100644 --- a/ezgui/src/widgets/mod.rs +++ b/ezgui/src/widgets/mod.rs @@ -17,7 +17,7 @@ pub(crate) use self::context_menu::ContextMenu; pub use self::modal_menu::ModalMenu; pub(crate) use self::popup_menu::PopupMenu; pub(crate) use self::screenshot::{screenshot_current, screenshot_everything}; -pub use self::scroller::Scroller; +pub use self::scroller::{NewScroller, Scroller}; pub use self::slider::{ItemSlider, Slider, SliderWithTextBox, WarpingItemSlider}; pub use self::warper::Warper; pub use self::wizard::{Choice, Wizard, WrappedWizard}; diff --git a/ezgui/src/widgets/scroller.rs b/ezgui/src/widgets/scroller.rs index d69d7750dc..ecfae11a43 100644 --- a/ezgui/src/widgets/scroller.rs +++ b/ezgui/src/widgets/scroller.rs @@ -1,4 +1,7 @@ -use crate::{Canvas, Color, EventCtx, GfxCtx, Line, ScreenDims, ScreenPt, ScreenRectangle, Text}; +use crate::{ + text, Canvas, Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiText, ScreenDims, + ScreenPt, ScreenRectangle, Text, +}; use geom::{Distance, Polygon, Pt2D}; use ordered_float::NotNan; @@ -266,3 +269,86 @@ impl Scroller { self.items.len() - 2 } } + +pub struct NewScroller { + draw: Drawable, + multi_txt: MultiText, + total_dims: ScreenDims, + zoom: f64, + + offset: f64, + + top_left: ScreenPt, + dims: ScreenDims, +} + +impl NewScroller { + // geom and multi_txt should be in screen-space, with the top_left as (0.0, 0.0). + pub fn new(geom: GeomBatch, multi_txt: MultiText, zoom: f64, ctx: &EventCtx) -> NewScroller { + let mut total_dims = geom.get_dims(); + for (txt, top_left) in &multi_txt.list { + let (mut w, mut h) = ctx.canvas.text_dims(txt); + w += top_left.x; + h += top_left.y; + if w > total_dims.width { + total_dims.width = w; + } + if h > total_dims.height { + total_dims.height = h; + } + } + + NewScroller { + draw: ctx.prerender.upload(geom), + multi_txt, + total_dims, + zoom, + + offset: 0.0, + + // TODO The layouting is hardcoded + top_left: ScreenPt::new(0.0, 0.0), + dims: ScreenDims::new(100.0, 100.0), + } + } + + pub fn event(&mut self, ctx: &mut EventCtx) { + let rect = ScreenRectangle::top_left(self.top_left, self.dims); + if rect.contains(ctx.canvas.get_cursor_in_screen_space()) { + if let Some(scroll) = ctx.input.get_mouse_scroll() { + self.offset -= scroll; + // TODO Clamp... or maybe last minute, based on dims, which'll get updated by + // window resizing and such + } + } + } + + pub fn draw(&self, g: &mut GfxCtx) { + let rect = ScreenRectangle::top_left(self.top_left, self.dims); + g.canvas.mark_covered_area(rect); + + g.fork_screenspace(); + g.draw_polygon( + text::BG_COLOR, + &Polygon::rectangle_topleft( + Pt2D::new(0.0, 0.0), + Distance::meters(self.dims.width), + Distance::meters(self.dims.height), + ), + ); + g.unfork(); + + g.fork(Pt2D::new(0.0, self.offset), self.top_left, self.zoom); + g.redraw(&self.draw); + g.unfork(); + + for (txt, pt) in &self.multi_txt.list { + g.draw_text_at_screenspace_topleft( + txt, + ScreenPt::new(pt.x, pt.y - self.offset * self.zoom), + ); + } + + // TODO draw scrollbar + } +} diff --git a/game/src/common/turn_cycler.rs b/game/src/common/turn_cycler.rs index 63f80bd25d..f14bf44cd5 100644 --- a/game/src/common/turn_cycler.rs +++ b/game/src/common/turn_cycler.rs @@ -1,6 +1,6 @@ use crate::game::{State, Transition}; use crate::helpers::ID; -use crate::render::{DrawCtx, DrawOptions, DrawTurn, TrafficSignalDiagram}; +use crate::render::{DrawOptions, DrawTurn, TrafficSignalDiagram}; use crate::ui::{ShowEverything, UI}; use ezgui::{hotkey, Color, EventCtx, GeomBatch, GfxCtx, Key, ModalMenu}; use map_model::{IntersectionID, LaneID, Map, TurnType}; @@ -48,14 +48,14 @@ impl TurnCyclerState { "Traffic Signal Diagram", vec![ vec![ - (hotkey(Key::UpArrow), "select previous cycle"), - (hotkey(Key::DownArrow), "select next cycle"), + (hotkey(Key::UpArrow), "select previous phase"), + (hotkey(Key::DownArrow), "select next phase"), ], vec![(hotkey(Key::Escape), "quit")], ], ctx, ), - diagram: TrafficSignalDiagram::new(i, idx, &ui.primary.map, ctx), + diagram: TrafficSignalDiagram::new(i, idx, ui, ctx), }))); } } @@ -144,13 +144,7 @@ impl State for ShowTrafficSignal { &ui.primary.sim, &ShowEverything::new(), ); - let ctx = DrawCtx { - cs: &ui.cs, - map: &ui.primary.map, - draw_map: &ui.primary.draw_map, - sim: &ui.primary.sim, - }; - self.diagram.draw(g, &ctx); + self.diagram.draw(g, &ui.draw_ctx()); self.menu.draw(g); } diff --git a/game/src/edit/mod.rs b/game/src/edit/mod.rs index f3ac967d10..19751c585b 100644 --- a/game/src/edit/mod.rs +++ b/game/src/edit/mod.rs @@ -6,7 +6,7 @@ use crate::debug::DebugMode; use crate::game::{State, Transition, WizardState}; use crate::helpers::{ColorScheme, ID}; use crate::render::{ - DrawCtx, DrawIntersection, DrawLane, DrawMap, DrawOptions, DrawRoad, DrawTurn, Renderable, + DrawIntersection, DrawLane, DrawMap, DrawOptions, DrawRoad, DrawTurn, Renderable, MIN_ZOOM_FOR_DETAIL, }; use crate::sandbox::SandboxMode; @@ -227,12 +227,7 @@ impl State for EditMode { // just show diff relative to basemap. let edits = ui.primary.map.get_edits(); - let ctx = DrawCtx { - cs: &ui.cs, - map: &ui.primary.map, - draw_map: &ui.primary.draw_map, - sim: &ui.primary.sim, - }; + let ctx = ui.draw_ctx(); let mut opts = DrawOptions::new(); // TODO Similar to drawing areas with traffic or not -- would be convenient to just diff --git a/game/src/edit/traffic_signals.rs b/game/src/edit/traffic_signals.rs index 8273811e57..bff258508f 100644 --- a/game/src/edit/traffic_signals.rs +++ b/game/src/edit/traffic_signals.rs @@ -2,7 +2,7 @@ use crate::common::CommonState; use crate::edit::apply_map_edits; use crate::game::{State, Transition, WizardState}; use crate::helpers::ID; -use crate::render::{draw_signal_phase, DrawCtx, DrawOptions, DrawTurn, TrafficSignalDiagram}; +use crate::render::{draw_signal_phase, DrawOptions, DrawTurn, TrafficSignalDiagram}; use crate::ui::{ShowEverything, UI}; use abstutil::Timer; use ezgui::{hotkey, Choice, Color, EventCtx, GeomBatch, GfxCtx, Key, ModalMenu}; @@ -49,7 +49,7 @@ impl TrafficSignalEditor { TrafficSignalEditor { menu, icon_selected: None, - diagram: TrafficSignalDiagram::new(id, 0, &ui.primary.map, ctx), + diagram: TrafficSignalDiagram::new(id, 0, ui, ctx), } } } @@ -132,7 +132,7 @@ impl State for TrafficSignalEditor { .remove(0) .1; change_traffic_signal(signal, self.diagram.i, ui, ctx); - self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, &ui.primary.map, ctx); + self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, ui, ctx); return Transition::Keep; } @@ -148,15 +148,13 @@ impl State for TrafficSignalEditor { if current_phase != 0 && self.menu.action("move current phase up") { signal.phases.swap(current_phase, current_phase - 1); change_traffic_signal(signal, self.diagram.i, ui, ctx); - self.diagram = - TrafficSignalDiagram::new(self.diagram.i, current_phase - 1, &ui.primary.map, ctx); + self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase - 1, ui, ctx); } else if current_phase != signal.phases.len() - 1 && self.menu.action("move current phase down") { signal.phases.swap(current_phase, current_phase + 1); change_traffic_signal(signal, self.diagram.i, ui, ctx); - self.diagram = - TrafficSignalDiagram::new(self.diagram.i, current_phase + 1, &ui.primary.map, ctx); + self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase + 1, ui, ctx); } else if signal.phases.len() > 1 && self.menu.action("delete current phase") { signal.phases.remove(current_phase); let num_phases = signal.phases.len(); @@ -168,7 +166,7 @@ impl State for TrafficSignalEditor { } else { current_phase }, - &ui.primary.map, + ui, ctx, ); } else if self.menu.action("add a new empty phase") { @@ -176,8 +174,7 @@ impl State for TrafficSignalEditor { .phases .insert(current_phase, Phase::new(self.diagram.i)); change_traffic_signal(signal, self.diagram.i, ui, ctx); - self.diagram = - TrafficSignalDiagram::new(self.diagram.i, current_phase, &ui.primary.map, ctx); + self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase, ui, ctx); } else if has_sidewalks && self.menu.action("add a new pedestrian scramble phase") { let mut phase = Phase::new(self.diagram.i); for t in ui.primary.map.get_turns_in_intersection(self.diagram.i) { @@ -187,8 +184,7 @@ impl State for TrafficSignalEditor { } signal.phases.insert(current_phase, phase); change_traffic_signal(signal, self.diagram.i, ui, ctx); - self.diagram = - TrafficSignalDiagram::new(self.diagram.i, current_phase, &ui.primary.map, ctx); + self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase, ui, ctx); } else if has_sidewalks && self .menu @@ -196,7 +192,7 @@ impl State for TrafficSignalEditor { { signal.convert_to_ped_scramble(&ui.primary.map); change_traffic_signal(signal, self.diagram.i, ui, ctx); - self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, &ui.primary.map, ctx); + self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, ui, ctx); } Transition::Keep @@ -210,12 +206,7 @@ impl State for TrafficSignalEditor { } let mut batch = GeomBatch::new(); - let ctx = DrawCtx { - cs: &ui.cs, - map: &ui.primary.map, - draw_map: &ui.primary.draw_map, - sim: &ui.primary.sim, - }; + let ctx = ui.draw_ctx(); let map = &ui.primary.map; let phase = &map.get_traffic_signal(self.diagram.i).phases[self.diagram.current_phase()]; for t in &ui.primary.draw_map.get_turns(self.diagram.i, map) { @@ -285,7 +276,7 @@ fn make_change_phase_duration(current_duration: Duration) -> Box { let idx = editor.diagram.current_phase(); signal.phases[idx].duration = Duration::seconds(new_duration as f64); change_traffic_signal(signal, editor.diagram.i, ui, ctx); - editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, idx, &ui.primary.map, ctx); + editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, idx, ui, ctx); }))) })) } @@ -303,7 +294,7 @@ fn make_change_preset(i: IntersectionID) -> Box { Some(Transition::PopWithData(Box::new(move |state, ui, ctx| { let mut editor = state.downcast_mut::().unwrap(); change_traffic_signal(new_signal, editor.diagram.i, ui, ctx); - editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, 0, &ui.primary.map, ctx); + editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, 0, ui, ctx); }))) })) } diff --git a/game/src/render/traffic_signal.rs b/game/src/render/traffic_signal.rs index 694e62d5c4..7b57ba64da 100644 --- a/game/src/render/traffic_signal.rs +++ b/game/src/render/traffic_signal.rs @@ -1,9 +1,11 @@ use crate::render::{DrawCtx, DrawTurn}; +use crate::ui::UI; use ezgui::{ - Color, EventCtx, GeomBatch, GfxCtx, Line, ModalMenu, ScreenDims, ScreenPt, Scroller, Text, + Color, EventCtx, GeomBatch, GfxCtx, Line, ModalMenu, MultiText, NewScroller, ScreenDims, + ScreenPt, Scroller, Text, }; use geom::{Circle, Distance, Duration, Line, Polygon, Pt2D}; -use map_model::{IntersectionID, Map, Phase, TurnPriority, TurnType, LANE_THICKNESS}; +use map_model::{IntersectionID, Phase, TurnPriority, TurnType, LANE_THICKNESS}; use ordered_float::NotNan; // Only draws a box when time_left is present @@ -221,7 +223,7 @@ fn draw_signal_phase_with_icons(phase: &Phase, batch: &mut GeomBatch, ctx: &Draw } const PADDING: f64 = 5.0; -const ZOOM: f64 = 10.0; +const ZOOM: f64 = 15.0; pub struct TrafficSignalDiagram { pub i: IntersectionID, @@ -230,17 +232,19 @@ pub struct TrafficSignalDiagram { intersection_width: f64, // TODO needed? // The usizes are phase indices scroller: Scroller, + + new_scroller: NewScroller, } impl TrafficSignalDiagram { pub fn new( i: IntersectionID, current_phase: usize, - map: &Map, + ui: &UI, ctx: &EventCtx, ) -> TrafficSignalDiagram { let (top_left, intersection_width, intersection_height) = { - let b = map.get_i(i).polygon.get_bounds(); + let b = ui.primary.map.get_i(i).polygon.get_bounds(); ( Pt2D::new(b.min_x, b.min_y), b.max_x - b.min_x, @@ -248,7 +252,7 @@ impl TrafficSignalDiagram { b.max_y - b.min_y, ) }; - let phases = &map.get_traffic_signal(i).phases; + let phases = &ui.primary.map.get_traffic_signal(i).phases; // Precalculate maximum text width. let mut labels = Vec::new(); @@ -285,6 +289,8 @@ impl TrafficSignalDiagram { top_left, intersection_width, scroller, + + new_scroller: make_new_scroller(i, &ui.draw_ctx(), ctx), } } @@ -301,6 +307,8 @@ impl TrafficSignalDiagram { self.scroller.select_next(ctx.canvas); return; } + + //self.new_scroller.event(ctx); } pub fn current_phase(&self) -> usize { @@ -324,5 +332,39 @@ impl TrafficSignalDiagram { } g.unfork(); + + //self.new_scroller.draw(g); } } + +fn make_new_scroller(i: IntersectionID, draw_ctx: &DrawCtx, ctx: &EventCtx) -> NewScroller { + // TODO Nicer API would be passing in a list of (GeomBatch, MultiText)s each starting at the + // origin, then do the translation later. + let mut master_batch = GeomBatch::new(); + let mut txt = MultiText::new(); + + // Slightly inaccurate -- the turn rendering may slightly exceed the intersection polygon -- + // but this is close enough. + let bounds = draw_ctx.map.get_i(i).polygon.get_bounds(); + let mut y_offset = 0.0; + for (idx, phase) in draw_ctx.map.get_traffic_signal(i).phases.iter().enumerate() { + let mut batch = GeomBatch::new(); + draw_signal_phase(phase, None, &mut batch, draw_ctx); + for (color, poly) in batch.consume() { + master_batch.push( + color, + poly.translate( + Distance::meters(-bounds.min_x), + Distance::meters(y_offset - bounds.min_y), + ), + ); + } + txt.add( + Text::from(Line(format!("Phase {}: {}", idx + 1, phase.duration))), + ScreenPt::new(10.0 + (bounds.max_x - bounds.min_x) * ZOOM, y_offset * ZOOM), + ); + y_offset += bounds.max_y - bounds.min_y; + } + + NewScroller::new(master_batch, txt, ZOOM, ctx) +} diff --git a/game/src/ui.rs b/game/src/ui.rs index bc1ac21753..ffde7ec092 100644 --- a/game/src/ui.rs +++ b/game/src/ui.rs @@ -80,6 +80,15 @@ impl UI { } } + pub fn draw_ctx<'a>(&'a self) -> DrawCtx<'a> { + DrawCtx { + cs: &self.cs, + map: &self.primary.map, + draw_map: &self.primary.draw_map, + sim: &self.primary.sim, + } + } + pub fn draw( &self, g: &mut GfxCtx, @@ -87,12 +96,7 @@ impl UI { source: &dyn GetDrawAgents, show_objs: &dyn ShowObject, ) { - let ctx = DrawCtx { - cs: &self.cs, - map: &self.primary.map, - draw_map: &self.primary.draw_map, - sim: &self.primary.sim, - }; + let ctx = self.draw_ctx(); let mut sample_intersection: Option = None; g.clear(self.cs.get_def("true background", Color::BLACK));