Individual hitboxes for multiple signals in the sidebar. #331

This commit is contained in:
Dustin Carlino 2020-09-19 11:52:15 -07:00
parent 3c11dca9ba
commit bb3342de27
4 changed files with 113 additions and 19 deletions

View File

@ -4,7 +4,7 @@ mod picker;
mod preview;
use crate::app::{App, ShowEverything};
use crate::common::CommonState;
use crate::common::{CommonState, Warping};
use crate::edit::{apply_map_edits, ConfirmDiscard};
use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
use crate::options::TrafficSignalStyle;
@ -19,7 +19,7 @@ use map_model::{
use std::collections::{BTreeSet, VecDeque};
use widgetry::{
lctrl, Btn, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line,
Outcome, Panel, RewriteColor, Text, TextExt, VerticalAlignment, Widget,
MultiButton, Outcome, Panel, RewriteColor, Text, TextExt, VerticalAlignment, Widget,
};
// Welcome to one of the most overwhelmingly complicated parts of the UI...
@ -247,9 +247,25 @@ impl State for TrafficSignalEditor {
return Transition::Keep;
}
if let Some(x) = x.strip_prefix("stage ") {
let idx = x.parse::<usize>().unwrap() - 1;
// 123, Intersection #456
let parts = x.split(", Intersection #").collect::<Vec<_>>();
let idx = parts[0].parse::<usize>().unwrap() - 1;
let i = IntersectionID(parts[1].parse::<usize>().unwrap());
self.change_stage(ctx, app, idx);
return Transition::Keep;
let center = app.primary.map.get_i(i).polygon.center();
// Constantly warping is really annoying, only do it if the intersection is
// offscreen
if ctx.canvas.get_screen_bounds().contains(center) {
return Transition::Keep;
} else {
return Transition::Push(Warping::new(
ctx,
center,
Some(15.0),
None,
&mut app.primary,
));
}
}
unreachable!()
}
@ -604,15 +620,7 @@ fn make_side_panel(
);
for (idx, canonical_stage) in canonical_signal.stages.iter().enumerate() {
let unselected_btn = draw_multiple_signals(ctx, app, members, idx, &translations);
let mut selected_btn = unselected_btn.clone();
let bbox = unselected_btn.get_bounds().get_rectangle();
selected_btn.push(Color::RED, bbox.to_outline(Distance::meters(5.0)).unwrap());
let stage_btn = Btn::custom(unselected_btn, selected_btn, bbox).build(
ctx,
format!("stage {}", idx + 1),
None,
);
let stage_btn = draw_multiple_signals(ctx, app, members, idx, &translations);
let stage_controls = Widget::row(vec![
Widget::col(vec![
@ -769,7 +777,7 @@ fn draw_multiple_signals(
members: &BTreeSet<IntersectionID>,
idx: usize,
translations: &Vec<(f64, f64)>,
) -> GeomBatch {
) -> Widget {
let mut batch = GeomBatch::new();
for (i, (dx, dy)) in members.iter().zip(translations) {
let mut piece = GeomBatch::new();
@ -791,10 +799,27 @@ fn draw_multiple_signals(
}
// Make the whole thing fit a fixed width
let bounds_before = batch.get_bounds();
batch = batch.autocrop();
let bounds = batch.get_bounds();
let zoom = (300.0 / bounds.width()).min(300.0 / bounds.height());
batch.scale(zoom)
let batch = batch.scale(zoom);
// Figure out the hitboxes per intersection, after all of these transformations
let mut hitboxes = Vec::new();
for (i, (dx, dy)) in members.iter().zip(translations) {
hitboxes.push((
app.primary
.map
.get_i(*i)
.polygon
.clone()
.translate(*dx - bounds_before.min_x, *dy - bounds_before.min_y)
.scale(zoom),
format!("stage {}, {}", idx + 1, i),
));
}
MultiButton::new(ctx, batch, hitboxes).named(format!("stage {}", idx + 1))
}
// TODO Move to geom?

View File

@ -254,7 +254,7 @@ impl Canvas {
)
}
//the inner bound tells us whether auto-panning should or should not take place
// the inner bound tells us whether auto-panning should or should not take place
fn get_inner_bounds(&self) -> Bounds {
let mut b = Bounds::new();
b.update(ScreenPt::new(PANNING_THRESHOLD, PANNING_THRESHOLD).to_pt());

View File

@ -16,6 +16,7 @@
//! * [`JustDraw`] (argh private) - just draw text, `GeomBatch`es, SVGs
//! * [`LinePlot`] - visualize 2 variables with a line plot
//! * [`Menu`] - select something from a menu, with keybindings
//! * [`MultiButton`] - clickable regions in one batch of geometry
//! * [`PersistentSplit`] - a button with a dropdown to change its state
//! * [`ScatterPlot`] - visualize 2 variables with a scatter plot
//! * [`Slider`] - horizontal and vertical sliders
@ -68,8 +69,8 @@ pub use crate::style::Style;
pub use crate::text::{Line, Text, TextExt, TextSpan};
pub use crate::tools::warper::Warper;
pub use crate::widgets::autocomplete::Autocomplete;
pub use crate::widgets::button::Btn;
pub(crate) use crate::widgets::button::Button;
pub use crate::widgets::button::{Btn, MultiButton};
pub use crate::widgets::checkbox::Checkbox;
pub use crate::widgets::compare_times::CompareTimes;
pub(crate) use crate::widgets::dropdown::Dropdown;

View File

@ -1,8 +1,8 @@
use crate::{
svg, Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiKey, Outcome, RewriteColor,
ScreenDims, ScreenPt, Text, Widget, WidgetImpl, WidgetOutput,
ScreenDims, ScreenPt, ScreenRectangle, Text, Widget, WidgetImpl, WidgetOutput,
};
use geom::Polygon;
use geom::{Distance, Polygon};
pub struct Button {
pub action: String,
@ -413,3 +413,71 @@ impl BtnBuilder {
}
}
}
// Like an image map from the old HTML days
pub struct MultiButton {
draw: Drawable,
hitboxes: Vec<(Polygon, String)>,
hovering: Option<usize>,
top_left: ScreenPt,
dims: ScreenDims,
}
impl MultiButton {
pub fn new(ctx: &EventCtx, batch: GeomBatch, hitboxes: Vec<(Polygon, String)>) -> Widget {
Widget::new(Box::new(MultiButton {
dims: batch.get_dims(),
top_left: ScreenPt::new(0.0, 0.0),
draw: ctx.upload(batch),
hitboxes,
hovering: None,
}))
}
}
impl WidgetImpl for MultiButton {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
if ctx.redo_mouseover() {
self.hovering = None;
if let Some(cursor) = ctx.canvas.get_cursor_in_screen_space() {
if !ScreenRectangle::top_left(self.top_left, self.dims).contains(cursor) {
return;
}
let translated =
ScreenPt::new(cursor.x - self.top_left.x, cursor.y - self.top_left.y).to_pt();
// TODO Assume regions are non-overlapping
for (idx, (region, _)) in self.hitboxes.iter().enumerate() {
if region.contains_pt(translated) {
self.hovering = Some(idx);
break;
}
}
}
}
if let Some(idx) = self.hovering {
if ctx.normal_left_click() {
self.hovering = None;
output.outcome = Outcome::Clicked(self.hitboxes[idx].1.clone());
}
}
}
fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw);
if let Some(idx) = self.hovering {
if let Ok(p) = self.hitboxes[idx].0.to_outline(Distance::meters(1.0)) {
let draw = g.upload(GeomBatch::from(vec![(Color::YELLOW, p)]));
g.redraw_at(self.top_left, &draw);
}
}
}
}