mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-24 09:24:26 +03:00
Make A/B Street's minimap specialize the generic widget, instead of duplicating code.
This commit is contained in:
parent
2153b1605b
commit
86814f6867
@ -1,427 +1,95 @@
|
||||
use abstutil::clamp;
|
||||
use geom::{Distance, Polygon, Pt2D, Ring};
|
||||
use map_gui::tools::Navigator;
|
||||
use widgetry::{
|
||||
Btn, Color, EventCtx, Filler, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome,
|
||||
Panel, ScreenPt, Spinner, VerticalAlignment, Widget,
|
||||
};
|
||||
use map_gui::tools::{MinimapControls, Navigator};
|
||||
use widgetry::{Btn, EventCtx, GfxCtx, HorizontalAlignment, Key, Panel, VerticalAlignment, Widget};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::Transition;
|
||||
use crate::common::Warping;
|
||||
use crate::layer::PickLayer;
|
||||
|
||||
// TODO Some of the math in here might assume map bound minimums start at (0, 0).
|
||||
pub struct Minimap {
|
||||
dragging: bool,
|
||||
pub(crate) panel: Panel,
|
||||
// Update panel when other things change
|
||||
zoomed: bool,
|
||||
layer: bool,
|
||||
pub struct MinimapController;
|
||||
|
||||
// [0, 3], with 0 meaning the most unzoomed
|
||||
zoom_lvl: usize,
|
||||
base_zoom: f64,
|
||||
zoom: f64,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
}
|
||||
|
||||
impl Minimap {
|
||||
pub fn new(ctx: &mut EventCtx, app: &App) -> Minimap {
|
||||
// Initially pick a zoom to fit the smaller of the entire map's width or height in the
|
||||
// minimap. Arbitrary and probably pretty weird.
|
||||
let bounds = app.primary.map.get_bounds();
|
||||
let base_zoom = 0.15 * ctx.canvas.window_width / bounds.width().min(bounds.height());
|
||||
let mut m = Minimap {
|
||||
dragging: false,
|
||||
panel: Panel::empty(ctx),
|
||||
zoomed: ctx.canvas.cam_zoom >= app.opts.min_zoom_for_detail,
|
||||
layer: app.primary.layer.is_none(),
|
||||
|
||||
zoom_lvl: 0,
|
||||
base_zoom,
|
||||
zoom: base_zoom,
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
};
|
||||
m.recreate_panel(ctx, app);
|
||||
if m.zoomed {
|
||||
m.recenter(ctx, app);
|
||||
}
|
||||
m
|
||||
impl MinimapControls<App> for MinimapController {
|
||||
fn has_zorder(&self, app: &App) -> bool {
|
||||
app.opts.dev
|
||||
}
|
||||
fn has_layer(&self, app: &App) -> bool {
|
||||
app.primary.layer.is_some()
|
||||
}
|
||||
|
||||
pub fn recreate_panel(&mut self, ctx: &mut EventCtx, app: &App) {
|
||||
if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
|
||||
self.panel = Panel::new(Widget::row(vec![
|
||||
make_tool_panel(ctx, app).align_right(),
|
||||
app.primary
|
||||
.agents
|
||||
.borrow()
|
||||
.unzoomed_agents
|
||||
.make_vert_viz_panel(ctx)
|
||||
.bg(app.cs.panel_bg)
|
||||
.padding(16),
|
||||
]))
|
||||
.aligned(
|
||||
HorizontalAlignment::Right,
|
||||
VerticalAlignment::BottomAboveOSD,
|
||||
)
|
||||
.build_custom(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
let zoom_col = {
|
||||
let mut col = vec![Btn::svg_def("system/assets/speed/speed_up.svg")
|
||||
.build(ctx, "zoom in", None)
|
||||
.margin_below(20)];
|
||||
for i in (0..=3).rev() {
|
||||
let color = if self.zoom_lvl < i {
|
||||
Color::WHITE.alpha(0.2)
|
||||
} else {
|
||||
Color::WHITE
|
||||
};
|
||||
let rect = Polygon::rectangle(20.0, 8.0);
|
||||
col.push(
|
||||
Btn::custom(
|
||||
GeomBatch::from(vec![(color, rect.clone())]),
|
||||
GeomBatch::from(vec![(app.cs.hovering, rect.clone())]),
|
||||
rect,
|
||||
None,
|
||||
)
|
||||
.build(ctx, format!("zoom to level {}", i + 1), None)
|
||||
.margin_below(20),
|
||||
);
|
||||
}
|
||||
col.push(
|
||||
Btn::svg_def("system/assets/speed/slow_down.svg").build(ctx, "zoom out", None),
|
||||
);
|
||||
// The zoom column should start below the "pan up" arrow. But if we put it on the row
|
||||
// with <, minimap, and > then it messes up the horizontal alignment of the
|
||||
// pan up arrow. Also, double column to avoid the background color
|
||||
// stretching to the bottom of the row.
|
||||
Widget::custom_col(vec![
|
||||
Widget::custom_col(col).padding(10).bg(app.cs.inner_panel),
|
||||
if app.opts.dev {
|
||||
Widget::col(vec![
|
||||
Line("Z-order:").small().draw(ctx),
|
||||
Spinner::new(
|
||||
ctx,
|
||||
app.primary.draw_map.zorder_range,
|
||||
app.primary.draw_map.show_zorder,
|
||||
)
|
||||
.named("zorder"),
|
||||
])
|
||||
.margin_above(10)
|
||||
} else {
|
||||
Widget::nothing()
|
||||
},
|
||||
])
|
||||
.margin_above(26)
|
||||
};
|
||||
|
||||
let minimap_controls = Widget::col(vec![
|
||||
Btn::svg_def("system/assets/minimap/up.svg")
|
||||
.build(ctx, "pan up", None)
|
||||
.centered_horiz(),
|
||||
Widget::row(vec![
|
||||
Btn::svg_def("system/assets/minimap/left.svg")
|
||||
.build(ctx, "pan left", None)
|
||||
.centered_vert(),
|
||||
Filler::square_width(ctx, 0.15).named("minimap"),
|
||||
Btn::svg_def("system/assets/minimap/right.svg")
|
||||
.build(ctx, "pan right", None)
|
||||
.centered_vert(),
|
||||
]),
|
||||
Btn::svg_def("system/assets/minimap/down.svg")
|
||||
.build(ctx, "pan down", None)
|
||||
.centered_horiz(),
|
||||
]);
|
||||
|
||||
self.panel = Panel::new(Widget::row(vec![
|
||||
make_tool_panel(ctx, app),
|
||||
Widget::col(vec![
|
||||
Widget::row(vec![minimap_controls, zoom_col]),
|
||||
app.primary
|
||||
.agents
|
||||
.borrow()
|
||||
.unzoomed_agents
|
||||
.make_horiz_viz_panel(ctx),
|
||||
])
|
||||
.padding(16)
|
||||
.bg(app.cs.panel_bg),
|
||||
]))
|
||||
.aligned(
|
||||
HorizontalAlignment::Right,
|
||||
VerticalAlignment::BottomAboveOSD,
|
||||
)
|
||||
.build_custom(ctx);
|
||||
}
|
||||
|
||||
fn map_to_minimap_pct(&self, pt: Pt2D) -> (f64, f64) {
|
||||
let inner_rect = self.panel.rect_of("minimap");
|
||||
let pct_x = (pt.x() * self.zoom - self.offset_x) / inner_rect.width();
|
||||
let pct_y = (pt.y() * self.zoom - self.offset_y) / inner_rect.height();
|
||||
(pct_x, pct_y)
|
||||
}
|
||||
|
||||
fn set_zoom(&mut self, ctx: &mut EventCtx, app: &App, zoom_lvl: usize) {
|
||||
// Make the frame wind up in the same relative position on the minimap
|
||||
let (pct_x, pct_y) = self.map_to_minimap_pct(ctx.canvas.center_to_map_pt());
|
||||
|
||||
let zoom_speed: f64 = 2.0;
|
||||
self.zoom_lvl = zoom_lvl;
|
||||
self.zoom = self.base_zoom * zoom_speed.powi(self.zoom_lvl as i32);
|
||||
self.recreate_panel(ctx, app);
|
||||
|
||||
// Find the new offset
|
||||
let map_center = ctx.canvas.center_to_map_pt();
|
||||
let inner_rect = self.panel.rect_of("minimap");
|
||||
self.offset_x = map_center.x() * self.zoom - pct_x * inner_rect.width();
|
||||
self.offset_y = map_center.y() * self.zoom - pct_y * inner_rect.height();
|
||||
}
|
||||
|
||||
fn recenter(&mut self, ctx: &EventCtx, app: &App) {
|
||||
// Recenter the minimap on the screen bounds
|
||||
let map_center = ctx.canvas.center_to_map_pt();
|
||||
let rect = self.panel.rect_of("minimap");
|
||||
let off_x = map_center.x() * self.zoom - rect.width() / 2.0;
|
||||
let off_y = map_center.y() * self.zoom - rect.height() / 2.0;
|
||||
|
||||
// Don't go out of bounds.
|
||||
let bounds = app.primary.map.get_bounds();
|
||||
// TODO For boundaries without rectangular shapes, it'd be even nicer to clamp to the
|
||||
// boundary.
|
||||
self.offset_x = off_x.max(0.0).min(bounds.max_x * self.zoom - rect.width());
|
||||
self.offset_y = off_y.max(0.0).min(bounds.max_y * self.zoom - rect.height());
|
||||
}
|
||||
|
||||
pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<Transition> {
|
||||
let zoomed = ctx.canvas.cam_zoom >= app.opts.min_zoom_for_detail;
|
||||
let layer = app.primary.layer.is_none();
|
||||
if zoomed != self.zoomed || layer != self.layer {
|
||||
let just_zoomed_in = zoomed && !self.zoomed;
|
||||
|
||||
self.zoomed = zoomed;
|
||||
self.layer = layer;
|
||||
self.recreate_panel(ctx, app);
|
||||
|
||||
if just_zoomed_in {
|
||||
self.recenter(ctx, app);
|
||||
}
|
||||
} else if self.zoomed && !self.dragging {
|
||||
// If either corner of the cursor is out of bounds on the minimap, recenter.
|
||||
// TODO This means clicking the pan buttons while along the boundary won't work.
|
||||
let mut ok = true;
|
||||
for pt in vec![
|
||||
ScreenPt::new(0.0, 0.0),
|
||||
ScreenPt::new(ctx.canvas.window_width, ctx.canvas.window_height),
|
||||
] {
|
||||
let (pct_x, pct_y) = self.map_to_minimap_pct(ctx.canvas.screen_to_map(pt));
|
||||
if pct_x < 0.0 || pct_x > 1.0 || pct_y < 0.0 || pct_y > 1.0 {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
self.recenter(ctx, app);
|
||||
}
|
||||
}
|
||||
if ctx.input.is_window_resized() {
|
||||
// When the window is resized, just reset completely. This is important when the window
|
||||
// size at startup is incorrect and immediately corrected by the window manager after
|
||||
// Minimap::new happens.
|
||||
let bounds = app.primary.map.get_bounds();
|
||||
// On Windows, apparently minimizing can cause some resize events with 0, 0 dimensions!
|
||||
self.base_zoom =
|
||||
(0.15 * ctx.canvas.window_width / bounds.width().min(bounds.height())).max(0.0001);
|
||||
self.zoom = self.base_zoom;
|
||||
if self.zoomed {
|
||||
self.recenter(ctx, app);
|
||||
}
|
||||
}
|
||||
|
||||
let pan_speed = 100.0;
|
||||
match self.panel.event(ctx) {
|
||||
Outcome::Clicked(x) => match x {
|
||||
x if x == "pan up" => {
|
||||
self.offset_y -= pan_speed * self.zoom;
|
||||
return Some(Transition::KeepWithMouseover);
|
||||
}
|
||||
x if x == "pan down" => {
|
||||
self.offset_y += pan_speed * self.zoom;
|
||||
return Some(Transition::KeepWithMouseover);
|
||||
}
|
||||
x if x == "pan left" => {
|
||||
self.offset_x -= pan_speed * self.zoom;
|
||||
return Some(Transition::KeepWithMouseover);
|
||||
}
|
||||
x if x == "pan right" => {
|
||||
self.offset_x += pan_speed * self.zoom;
|
||||
return Some(Transition::KeepWithMouseover);
|
||||
}
|
||||
// TODO Make the center of the cursor still point to the same thing. Same math as
|
||||
// Canvas.
|
||||
x if x == "zoom in" => {
|
||||
if self.zoom_lvl != 3 {
|
||||
self.set_zoom(ctx, app, self.zoom_lvl + 1);
|
||||
}
|
||||
}
|
||||
x if x == "zoom out" => {
|
||||
if self.zoom_lvl != 0 {
|
||||
self.set_zoom(ctx, app, self.zoom_lvl - 1);
|
||||
}
|
||||
}
|
||||
x if x == "zoom to level 1" => {
|
||||
self.set_zoom(ctx, app, 0);
|
||||
}
|
||||
x if x == "zoom to level 2" => {
|
||||
self.set_zoom(ctx, app, 1);
|
||||
}
|
||||
x if x == "zoom to level 3" => {
|
||||
self.set_zoom(ctx, app, 2);
|
||||
}
|
||||
x if x == "zoom to level 4" => {
|
||||
self.set_zoom(ctx, app, 3);
|
||||
}
|
||||
x if x == "search" => {
|
||||
return Some(Transition::Push(Navigator::new(ctx, app)));
|
||||
}
|
||||
x if x == "zoom out fully" => {
|
||||
return Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
app.primary.map.get_bounds().get_rectangle().center(),
|
||||
Some(ctx.canvas.min_zoom()),
|
||||
None,
|
||||
&mut app.primary,
|
||||
)));
|
||||
}
|
||||
x if x == "zoom in fully" => {
|
||||
return Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
ctx.canvas.center_to_map_pt(),
|
||||
Some(10.0),
|
||||
None,
|
||||
&mut app.primary,
|
||||
)));
|
||||
}
|
||||
x if x == "change layers" => {
|
||||
return Some(Transition::Push(PickLayer::pick(ctx, app)));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Outcome::Changed => {
|
||||
if self.panel.has_widget("Car") {
|
||||
app.primary
|
||||
.agents
|
||||
.borrow_mut()
|
||||
.unzoomed_agents
|
||||
.update(&self.panel);
|
||||
}
|
||||
if self.panel.has_widget("zorder") {
|
||||
app.primary.draw_map.show_zorder = self.panel.spinner("zorder");
|
||||
}
|
||||
self.recreate_panel(ctx, app);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.zoomed {
|
||||
let inner_rect = self.panel.rect_of("minimap");
|
||||
|
||||
// TODO Not happy about reaching in like this. The minimap logic should be an widgetry
|
||||
// Widget eventually, a generalization of Canvas.
|
||||
let mut pt = ctx.canvas.get_cursor();
|
||||
if self.dragging {
|
||||
if ctx.input.left_mouse_button_released() {
|
||||
self.dragging = false;
|
||||
}
|
||||
// Don't drag out of inner_rect
|
||||
pt.x = clamp(pt.x, inner_rect.x1, inner_rect.x2);
|
||||
pt.y = clamp(pt.y, inner_rect.y1, inner_rect.y2);
|
||||
} else if inner_rect.contains(pt) && ctx.input.left_mouse_button_pressed() {
|
||||
self.dragging = true;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
let percent_x = (pt.x - inner_rect.x1) / inner_rect.width();
|
||||
let percent_y = (pt.y - inner_rect.y1) / inner_rect.height();
|
||||
|
||||
let map_pt = Pt2D::new(
|
||||
(self.offset_x + percent_x * inner_rect.width()) / self.zoom,
|
||||
(self.offset_y + percent_y * inner_rect.height()) / self.zoom,
|
||||
);
|
||||
ctx.canvas.center_on_map_pt(map_pt);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx, app: &App) {
|
||||
self.panel.draw(g);
|
||||
if !self.zoomed {
|
||||
return;
|
||||
}
|
||||
|
||||
let inner_rect = self.panel.rect_of("minimap").clone();
|
||||
|
||||
let mut map_bounds = app.primary.map.get_bounds().clone();
|
||||
// Adjust bounds to account for the current pan and zoom
|
||||
map_bounds.min_x = (map_bounds.min_x + self.offset_x) / self.zoom;
|
||||
map_bounds.min_y = (map_bounds.min_y + self.offset_y) / self.zoom;
|
||||
map_bounds.max_x = map_bounds.min_x + inner_rect.width() / self.zoom;
|
||||
map_bounds.max_y = map_bounds.min_y + inner_rect.height() / self.zoom;
|
||||
|
||||
g.fork(
|
||||
Pt2D::new(map_bounds.min_x, map_bounds.min_y),
|
||||
ScreenPt::new(inner_rect.x1, inner_rect.y1),
|
||||
self.zoom,
|
||||
None,
|
||||
);
|
||||
g.enable_clipping(inner_rect);
|
||||
g.redraw(&app.primary.draw_map.boundary_polygon);
|
||||
g.redraw(&app.primary.draw_map.draw_all_areas);
|
||||
g.redraw(&app.primary.draw_map.draw_all_unzoomed_parking_lots);
|
||||
g.redraw(
|
||||
&app.primary
|
||||
.draw_map
|
||||
.draw_all_unzoomed_roads_and_intersections,
|
||||
);
|
||||
g.redraw(&app.primary.draw_map.draw_all_buildings);
|
||||
// Not the building or parking lot paths
|
||||
fn draw_extra(&self, g: &mut GfxCtx, app: &App) {
|
||||
if let Some(ref l) = app.primary.layer {
|
||||
l.draw_minimap(g);
|
||||
}
|
||||
|
||||
let mut cache = app.primary.agents.borrow_mut();
|
||||
cache.draw_unzoomed_agents(g, app);
|
||||
}
|
||||
|
||||
// The cursor
|
||||
let (x1, y1) = {
|
||||
let pt = g.canvas.screen_to_map(ScreenPt::new(0.0, 0.0));
|
||||
(pt.x(), pt.y())
|
||||
};
|
||||
let (x2, y2) = {
|
||||
let pt = g
|
||||
.canvas
|
||||
.screen_to_map(ScreenPt::new(g.canvas.window_width, g.canvas.window_height));
|
||||
(pt.x(), pt.y())
|
||||
};
|
||||
g.draw_polygon(
|
||||
app.cs.minimap_cursor,
|
||||
Ring::must_new(vec![
|
||||
Pt2D::new(x1, y1),
|
||||
Pt2D::new(x2, y1),
|
||||
Pt2D::new(x2, y2),
|
||||
Pt2D::new(x1, y2),
|
||||
Pt2D::new(x1, y1),
|
||||
])
|
||||
.to_outline(Distance::meters(20.0)),
|
||||
);
|
||||
g.disable_clipping();
|
||||
g.unfork();
|
||||
fn make_unzoomed_panel(&self, ctx: &mut EventCtx, app: &App) -> Panel {
|
||||
Panel::new(Widget::row(vec![
|
||||
make_tool_panel(ctx, app).align_right(),
|
||||
app.primary
|
||||
.agents
|
||||
.borrow()
|
||||
.unzoomed_agents
|
||||
.make_vert_viz_panel(ctx)
|
||||
.bg(app.cs.panel_bg)
|
||||
.padding(16),
|
||||
]))
|
||||
.aligned(
|
||||
HorizontalAlignment::Right,
|
||||
VerticalAlignment::BottomAboveOSD,
|
||||
)
|
||||
.build_custom(ctx)
|
||||
}
|
||||
fn make_legend(&self, ctx: &mut EventCtx, app: &App) -> Widget {
|
||||
app.primary
|
||||
.agents
|
||||
.borrow()
|
||||
.unzoomed_agents
|
||||
.make_horiz_viz_panel(ctx)
|
||||
}
|
||||
fn make_zoomed_side_panel(&self, ctx: &mut EventCtx, app: &App) -> Widget {
|
||||
make_tool_panel(ctx, app)
|
||||
}
|
||||
|
||||
fn panel_clicked(&self, ctx: &mut EventCtx, app: &mut App, action: &str) -> Option<Transition> {
|
||||
match action {
|
||||
x if x == "search" => {
|
||||
return Some(Transition::Push(Navigator::new(ctx, app)));
|
||||
}
|
||||
x if x == "zoom out fully" => {
|
||||
return Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
app.primary.map.get_bounds().get_rectangle().center(),
|
||||
Some(ctx.canvas.min_zoom()),
|
||||
None,
|
||||
&mut app.primary,
|
||||
)));
|
||||
}
|
||||
x if x == "zoom in fully" => {
|
||||
return Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
ctx.canvas.center_to_map_pt(),
|
||||
Some(10.0),
|
||||
None,
|
||||
&mut app.primary,
|
||||
)));
|
||||
}
|
||||
x if x == "change layers" => {
|
||||
return Some(Transition::Push(PickLayer::pick(ctx, app)));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
fn panel_changed(&self, _: &mut EventCtx, app: &mut App, panel: &Panel) {
|
||||
if panel.has_widget("Car") {
|
||||
app.primary
|
||||
.agents
|
||||
.borrow_mut()
|
||||
.unzoomed_agents
|
||||
.update(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ use widgetry::{
|
||||
VerticalAlignment, Widget,
|
||||
};
|
||||
|
||||
pub use self::minimap::Minimap;
|
||||
pub use self::minimap::MinimapController;
|
||||
pub use self::warp::Warping;
|
||||
use crate::app::App;
|
||||
use crate::app::Transition;
|
||||
|
@ -2,7 +2,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
use abstutil::Timer;
|
||||
use geom::{ArrowCap, Distance, Duration, PolyLine, Pt2D, Time};
|
||||
use map_gui::tools::{grey_out_map, PopupMsg};
|
||||
use map_gui::tools::{grey_out_map, Minimap, PopupMsg};
|
||||
use map_gui::ID;
|
||||
use map_model::raw::OriginalRoad;
|
||||
use map_model::{osm, BuildingID, Map, Position};
|
||||
@ -17,7 +17,7 @@ use widgetry::{
|
||||
|
||||
use crate::app::{App, Transition};
|
||||
use crate::challenges::cutscene::CutsceneBuilder;
|
||||
use crate::common::{tool_panel, Minimap, Warping};
|
||||
use crate::common::{tool_panel, MinimapController, Warping};
|
||||
use crate::edit::EditMode;
|
||||
use crate::sandbox::gameplay::{GameplayMode, GameplayState};
|
||||
use crate::sandbox::{
|
||||
@ -902,7 +902,7 @@ impl TutorialState {
|
||||
// The minimap is hidden at low zoom levels
|
||||
let orig_zoom = ctx.canvas.cam_zoom;
|
||||
ctx.canvas.cam_zoom = 100.0;
|
||||
let minimap = Minimap::new(ctx, app);
|
||||
let minimap = Minimap::new(ctx, app, MinimapController);
|
||||
ctx.canvas.cam_zoom = orig_zoom;
|
||||
|
||||
let map = &app.primary.map;
|
||||
@ -1170,7 +1170,7 @@ impl TutorialState {
|
||||
overview of all activity. You can click and drag it just like the normal \
|
||||
map.",
|
||||
],
|
||||
arrow(minimap.panel.center_of("minimap")),
|
||||
arrow(minimap.get_panel().center_of("minimap")),
|
||||
)
|
||||
.msg(
|
||||
vec![
|
||||
@ -1180,7 +1180,7 @@ impl TutorialState {
|
||||
"- bus stops",
|
||||
"- how much parking is filled up",
|
||||
],
|
||||
arrow(minimap.panel.center_of("change layers")),
|
||||
arrow(minimap.get_panel().center_of("change layers")),
|
||||
)
|
||||
.msg(
|
||||
vec![
|
||||
|
@ -6,7 +6,7 @@ pub use time_warp::TimeWarpScreen;
|
||||
use abstutil::prettyprint_usize;
|
||||
use geom::{Circle, Distance, Pt2D, Time};
|
||||
use map_gui::load::{FileLoader, MapLoader};
|
||||
use map_gui::tools::{ChooseSomething, PopupMsg, TurnExplorer};
|
||||
use map_gui::tools::{ChooseSomething, Minimap, PopupMsg, TurnExplorer};
|
||||
use map_gui::AppLike;
|
||||
use map_gui::ID;
|
||||
use sim::{Analytics, Scenario};
|
||||
@ -17,7 +17,7 @@ use widgetry::{
|
||||
|
||||
use self::misc_tools::{RoutePreview, TrafficRecorder};
|
||||
use crate::app::{App, Transition};
|
||||
use crate::common::{tool_panel, CommonState, Minimap};
|
||||
use crate::common::{tool_panel, CommonState, MinimapController};
|
||||
use crate::debug::DebugMode;
|
||||
use crate::edit::{
|
||||
can_edit_lane, EditMode, LaneEditor, SaveEdits, StopSignEditor, TrafficSignalEditor,
|
||||
@ -52,7 +52,7 @@ pub struct SandboxControls {
|
||||
time_panel: Option<TimePanel>,
|
||||
speed: Option<SpeedControls>,
|
||||
pub agent_meter: Option<AgentMeter>,
|
||||
minimap: Option<Minimap>,
|
||||
minimap: Option<Minimap<App, MinimapController>>,
|
||||
}
|
||||
|
||||
impl SandboxMode {
|
||||
@ -140,7 +140,7 @@ impl State<App> for SandboxMode {
|
||||
if let Some(t) = m.event(ctx, app) {
|
||||
return t;
|
||||
}
|
||||
if let Some(t) = PickLayer::update(ctx, app, &m.panel) {
|
||||
if let Some(t) = PickLayer::update(ctx, app, m.get_panel()) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@ -874,7 +874,7 @@ impl SandboxControls {
|
||||
None
|
||||
},
|
||||
minimap: if gameplay.has_minimap() {
|
||||
Some(Minimap::new(ctx, app))
|
||||
Some(Minimap::new(ctx, app, MinimapController))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
@ -7,7 +7,6 @@ use widgetry::{
|
||||
Panel, ScreenPt, Spinner, Transition, VerticalAlignment, Widget,
|
||||
};
|
||||
|
||||
use crate::tools::Navigator;
|
||||
use crate::AppLike;
|
||||
|
||||
// TODO Some of the math in here might assume map bound minimums start at (0, 0).
|
||||
@ -16,9 +15,10 @@ pub struct Minimap<A: AppLike, T: MinimapControls<A>> {
|
||||
app_type: PhantomData<A>,
|
||||
|
||||
dragging: bool,
|
||||
pub(crate) panel: Panel,
|
||||
panel: Panel,
|
||||
// Update panel when other things change
|
||||
zoomed: bool,
|
||||
layer: bool,
|
||||
|
||||
// [0, 3], with 0 meaning the most unzoomed
|
||||
zoom_lvl: usize,
|
||||
@ -28,9 +28,40 @@ pub struct Minimap<A: AppLike, T: MinimapControls<A>> {
|
||||
offset_y: f64,
|
||||
}
|
||||
|
||||
/// Customize the appearance and behavior of a minimap.
|
||||
pub trait MinimapControls<A: AppLike> {
|
||||
/// Should the user be able to control the z-order visible? The control is only present when
|
||||
/// zoomed in, placed beneath the zoom column.
|
||||
fn has_zorder(&self, app: &A) -> bool;
|
||||
fn make_legend(&self, ctx: &mut EventCtx, app: &A) -> Widget;
|
||||
/// Is there some additional layer displayed on the minimap? If this changes, the panel gets
|
||||
/// recalculated.
|
||||
fn has_layer(&self, _: &A) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Draw extra stuff on the minimap, just pulling from the app.
|
||||
fn draw_extra(&self, _: &mut GfxCtx, _: &A) {}
|
||||
|
||||
/// When unzoomed, display this panel. By default, no controls when unzoomed.
|
||||
fn make_unzoomed_panel(&self, ctx: &mut EventCtx, _: &A) -> Panel {
|
||||
Panel::empty(ctx)
|
||||
}
|
||||
/// A row beneath the minimap in the zoomed view, usually used as a legend for things on the
|
||||
/// minimap.
|
||||
fn make_legend(&self, _: &mut EventCtx, _: &A) -> Widget {
|
||||
Widget::nothing()
|
||||
}
|
||||
/// Controls to be placed to the left to the zoomed-in panel
|
||||
fn make_zoomed_side_panel(&self, _: &mut EventCtx, _: &A) -> Widget {
|
||||
Widget::nothing()
|
||||
}
|
||||
|
||||
/// If a button is clicked that was produced by some method in this trait, respond to it here.
|
||||
fn panel_clicked(&self, _: &mut EventCtx, _: &mut A, _: &str) -> Option<Transition<A>> {
|
||||
unreachable!()
|
||||
}
|
||||
/// Called for `Outcome::Changed` on the panel.
|
||||
fn panel_changed(&self, _: &mut EventCtx, _: &mut A, _: &Panel) {}
|
||||
}
|
||||
|
||||
impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
@ -39,6 +70,7 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
// minimap. Arbitrary and probably pretty weird.
|
||||
let bounds = app.map().get_bounds();
|
||||
let base_zoom = 0.15 * ctx.canvas.window_width / bounds.width().min(bounds.height());
|
||||
let layer = controls.has_layer(app);
|
||||
let mut m = Minimap {
|
||||
controls,
|
||||
app_type: PhantomData,
|
||||
@ -46,6 +78,7 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
dragging: false,
|
||||
panel: Panel::empty(ctx),
|
||||
zoomed: ctx.canvas.cam_zoom >= app.opts().min_zoom_for_detail,
|
||||
layer,
|
||||
|
||||
zoom_lvl: 0,
|
||||
base_zoom,
|
||||
@ -62,7 +95,7 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
|
||||
pub fn recreate_panel(&mut self, ctx: &mut EventCtx, app: &A) {
|
||||
if ctx.canvas.cam_zoom < app.opts().min_zoom_for_detail {
|
||||
self.panel = Panel::empty(ctx);
|
||||
self.panel = self.controls.make_unzoomed_panel(ctx, app);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -129,12 +162,15 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
.centered_horiz(),
|
||||
]);
|
||||
|
||||
self.panel = Panel::new(Widget::row(vec![Widget::col(vec![
|
||||
Widget::row(vec![minimap_controls, zoom_col]),
|
||||
self.controls.make_legend(ctx, app),
|
||||
])
|
||||
.padding(16)
|
||||
.bg(app.cs().panel_bg)]))
|
||||
self.panel = Panel::new(Widget::row(vec![
|
||||
self.controls.make_zoomed_side_panel(ctx, app),
|
||||
Widget::col(vec![
|
||||
Widget::row(vec![minimap_controls, zoom_col]),
|
||||
self.controls.make_legend(ctx, app),
|
||||
])
|
||||
.padding(16)
|
||||
.bg(app.cs().panel_bg),
|
||||
]))
|
||||
.aligned(
|
||||
HorizontalAlignment::Right,
|
||||
VerticalAlignment::BottomAboveOSD,
|
||||
@ -182,10 +218,12 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
|
||||
pub fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Option<Transition<A>> {
|
||||
let zoomed = ctx.canvas.cam_zoom >= app.opts().min_zoom_for_detail;
|
||||
if zoomed != self.zoomed {
|
||||
let layer = self.controls.has_layer(app);
|
||||
if zoomed != self.zoomed || layer != self.layer {
|
||||
let just_zoomed_in = zoomed && !self.zoomed;
|
||||
|
||||
self.zoomed = zoomed;
|
||||
self.layer = layer;
|
||||
self.recreate_panel(ctx, app);
|
||||
|
||||
if just_zoomed_in {
|
||||
@ -266,13 +304,17 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
x if x == "zoom to level 4" => {
|
||||
self.set_zoom(ctx, app, 3);
|
||||
}
|
||||
x if x == "search" => {
|
||||
return Some(Transition::Push(Navigator::new(ctx, app)));
|
||||
x => {
|
||||
if let Some(transition) = self.controls.panel_clicked(ctx, app, &x) {
|
||||
return Some(transition);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Outcome::Changed => {
|
||||
app.mut_draw_map().show_zorder = self.panel.spinner("zorder");
|
||||
self.controls.panel_changed(ctx, app, &self.panel);
|
||||
if self.panel.has_widget("zorder") {
|
||||
app.mut_draw_map().show_zorder = self.panel.spinner("zorder");
|
||||
}
|
||||
self.recreate_panel(ctx, app);
|
||||
}
|
||||
_ => {}
|
||||
@ -345,6 +387,7 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
for draw in extra {
|
||||
g.redraw(draw);
|
||||
}
|
||||
self.controls.draw_extra(g, app);
|
||||
// Not the building or parking lot paths
|
||||
|
||||
// The cursor
|
||||
@ -372,4 +415,8 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
|
||||
g.disable_clipping();
|
||||
g.unfork();
|
||||
}
|
||||
|
||||
pub fn get_panel(&self) -> &Panel {
|
||||
&self.panel
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user