mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-24 01:15:12 +03:00
Steps towards clicking cells in the problem matrix to explore trips with differences. #600
- Add optional clickable labels to DrawWithTooltips - Wire up problem_matrix to remember the list of trips associated with each cell - When clicking a cell, just open one arbitrary example trip
This commit is contained in:
parent
db435cd56d
commit
942f2292fc
@ -200,7 +200,7 @@ fn current_demand_body(ctx: &mut EventCtx, app: &App, id: IntersectionID) -> Wid
|
||||
polygon.translate(-bounds.min_x, -bounds.min_y).scale(zoom),
|
||||
);
|
||||
|
||||
let mut tooltips: Vec<(Polygon, Text)> = Vec::new();
|
||||
let mut tooltips: Vec<(Polygon, Text, Option<String>)> = Vec::new();
|
||||
let mut outlines = Vec::new();
|
||||
for (pl, demand) in demand_per_movement {
|
||||
let percent = (demand as f64) / (total_demand as f64);
|
||||
@ -212,7 +212,7 @@ fn current_demand_body(ctx: &mut EventCtx, app: &App, id: IntersectionID) -> Wid
|
||||
outlines.push(p);
|
||||
}
|
||||
batch.push(Color::hex("#A3A3A3"), arrow.clone());
|
||||
tooltips.push((arrow, Text::from(prettyprint_usize(demand))));
|
||||
tooltips.push((arrow, Text::from(prettyprint_usize(demand)), None));
|
||||
}
|
||||
batch.extend(Color::WHITE, outlines);
|
||||
|
||||
|
@ -612,7 +612,7 @@ fn make_timeline(
|
||||
// icons above them.
|
||||
let mut batch = GeomBatch::new();
|
||||
// And associate a tooltip with each rectangle segment
|
||||
let mut tooltips: Vec<(Polygon, Text)> = Vec::new();
|
||||
let mut tooltips: Vec<(Polygon, Text, Option<String>)> = Vec::new();
|
||||
// How far along are we from previous segments?
|
||||
let mut x1 = 0.0;
|
||||
let rectangle_height = 15.0;
|
||||
@ -660,6 +660,7 @@ fn make_timeline(
|
||||
tooltips.push((
|
||||
rectangle.clone(),
|
||||
Text::from_multiline(tooltip.into_iter().map(Line).collect()),
|
||||
None,
|
||||
));
|
||||
|
||||
batch.push(
|
||||
|
@ -1,19 +1,21 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use map_gui::tools::PopupMsg;
|
||||
use sim::TripMode;
|
||||
use sim::{TripID, TripMode};
|
||||
use widgetry::{EventCtx, GfxCtx, Image, Line, Outcome, Panel, State, TextExt, Toggle, Widget};
|
||||
|
||||
use super::trip_problems::{problem_matrix, ProblemType, TripProblemFilter};
|
||||
use crate::app::{App, Transition};
|
||||
use crate::sandbox::dashboards::generic_trip_table::open_trip_transition;
|
||||
use crate::sandbox::dashboards::DashTab;
|
||||
|
||||
pub struct RiskSummaries {
|
||||
panel: Panel,
|
||||
trip_lookup: HashMap<String, Vec<TripID>>,
|
||||
}
|
||||
|
||||
impl RiskSummaries {
|
||||
@ -26,12 +28,33 @@ impl RiskSummaries {
|
||||
modes: maplit::btreeset! { TripMode::Bike },
|
||||
include_no_changes,
|
||||
};
|
||||
|
||||
let ped_filter = Filter {
|
||||
modes: maplit::btreeset! { TripMode::Walk },
|
||||
include_no_changes,
|
||||
};
|
||||
|
||||
let (arterial_problems, mut trip_lookup) = problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
"arterials",
|
||||
ped_filter.trip_problems(app, ProblemType::ArterialIntersectionCrossing),
|
||||
);
|
||||
let (complex_intersection_problems, lookup2) = problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
"complex intersections",
|
||||
bike_filter.trip_problems(app, ProblemType::ComplexIntersectionCrossing),
|
||||
);
|
||||
let (overtaking_problems, lookup3) = problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
"overtakings",
|
||||
bike_filter.trip_problems(app, ProblemType::OvertakeDesired),
|
||||
);
|
||||
// The keys won't overlap, due to the unique title of each matrix.
|
||||
trip_lookup.extend(lookup2);
|
||||
trip_lookup.extend(lookup3);
|
||||
|
||||
Box::new(RiskSummaries {
|
||||
panel: Panel::new_builder(Widget::col(vec![
|
||||
DashTab::RiskSummaries.picker(ctx, app),
|
||||
@ -66,12 +89,7 @@ impl RiskSummaries {
|
||||
.small_heading()
|
||||
.into_widget(ctx)
|
||||
.centered_horiz(),
|
||||
problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
&ped_filter
|
||||
.trip_problems(app, ProblemType::ArterialIntersectionCrossing),
|
||||
),
|
||||
arterial_problems,
|
||||
])
|
||||
.section(ctx)],
|
||||
)
|
||||
@ -98,12 +116,7 @@ impl RiskSummaries {
|
||||
.small_heading()
|
||||
.into_widget(ctx)
|
||||
.centered_horiz(),
|
||||
problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
&bike_filter
|
||||
.trip_problems(app, ProblemType::ComplexIntersectionCrossing),
|
||||
),
|
||||
complex_intersection_problems,
|
||||
])
|
||||
.section(ctx),
|
||||
Widget::col(vec![
|
||||
@ -111,11 +124,7 @@ impl RiskSummaries {
|
||||
.small_heading()
|
||||
.into_widget(ctx)
|
||||
.centered_horiz(),
|
||||
problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
&bike_filter.trip_problems(app, ProblemType::OvertakeDesired),
|
||||
),
|
||||
overtaking_problems,
|
||||
])
|
||||
.section(ctx),
|
||||
],
|
||||
@ -125,6 +134,7 @@ impl RiskSummaries {
|
||||
]))
|
||||
.exact_size_percent(90, 90)
|
||||
.build(ctx),
|
||||
trip_lookup,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -146,7 +156,10 @@ impl State<App> for RiskSummaries {
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => unreachable!(),
|
||||
x => {
|
||||
// TODO Handle browsing multiple trips
|
||||
return open_trip_transition(app, self.trip_lookup[x][0].0);
|
||||
}
|
||||
},
|
||||
Outcome::Changed(_) => {
|
||||
if let Some(t) = DashTab::RiskSummaries.transition(ctx, app, &self.panel) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
@ -7,7 +7,7 @@ use anyhow::Result;
|
||||
use abstutil::prettyprint_usize;
|
||||
use geom::{Distance, Duration, Polygon, Pt2D};
|
||||
use map_gui::tools::PopupMsg;
|
||||
use sim::TripMode;
|
||||
use sim::{TripID, TripMode};
|
||||
use widgetry::{
|
||||
Choice, Color, CompareTimes, DrawWithTooltips, EventCtx, GeomBatch, GfxCtx, Line, Outcome,
|
||||
Panel, State, Text, TextExt, Toggle, Widget,
|
||||
@ -16,20 +16,20 @@ use widgetry::{
|
||||
use super::trip_problems::{problem_matrix, ProblemType, TripProblemFilter};
|
||||
use crate::app::{App, Transition};
|
||||
use crate::common::color_for_mode;
|
||||
use crate::sandbox::dashboards::generic_trip_table::open_trip_transition;
|
||||
use crate::sandbox::dashboards::DashTab;
|
||||
|
||||
pub struct TravelTimes {
|
||||
panel: Panel,
|
||||
trip_lookup: HashMap<String, Vec<TripID>>,
|
||||
}
|
||||
|
||||
impl TravelTimes {
|
||||
pub fn new_state(ctx: &mut EventCtx, app: &App, filter: Filter) -> Box<dyn State<App>> {
|
||||
Box::new(TravelTimes {
|
||||
panel: TravelTimes::make_panel(ctx, app, filter),
|
||||
})
|
||||
Box::new(TravelTimes::new(ctx, app, filter))
|
||||
}
|
||||
|
||||
fn make_panel(ctx: &mut EventCtx, app: &App, filter: Filter) -> Panel {
|
||||
fn new(ctx: &mut EventCtx, app: &App, filter: Filter) -> TravelTimes {
|
||||
let mut filters = vec!["Filters".text_widget(ctx)];
|
||||
for mode in TripMode::all() {
|
||||
filters.push(Toggle::colored_checkbox(
|
||||
@ -48,7 +48,14 @@ impl TravelTimes {
|
||||
.align_bottom(),
|
||||
);
|
||||
|
||||
Panel::new_builder(Widget::col(vec![
|
||||
let (problems, trip_lookup) = problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
"delays",
|
||||
filter.trip_problems(app, ProblemType::IntersectionDelay),
|
||||
);
|
||||
|
||||
let panel = Panel::new_builder(Widget::col(vec![
|
||||
DashTab::TravelTimes.picker(ctx, app),
|
||||
Widget::row(vec![
|
||||
Widget::col(filters).section(ctx),
|
||||
@ -92,19 +99,16 @@ impl TravelTimes {
|
||||
filter.include_no_changes(),
|
||||
),
|
||||
]),
|
||||
problem_matrix(
|
||||
ctx,
|
||||
app,
|
||||
&filter.trip_problems(app, ProblemType::IntersectionDelay),
|
||||
)
|
||||
.margin_left(32),
|
||||
problems.margin_left(32),
|
||||
])
|
||||
.section(ctx),
|
||||
]),
|
||||
]),
|
||||
]))
|
||||
.exact_size_percent(90, 90)
|
||||
.build(ctx)
|
||||
.build(ctx);
|
||||
|
||||
TravelTimes { panel, trip_lookup }
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +129,10 @@ impl State<App> for TravelTimes {
|
||||
});
|
||||
}
|
||||
"close" => Transition::Pop,
|
||||
_ => unreachable!(),
|
||||
x => {
|
||||
// TODO Handle browsing multiple trips
|
||||
return open_trip_transition(app, self.trip_lookup[x][0].0);
|
||||
}
|
||||
},
|
||||
Outcome::Changed(_) => {
|
||||
if let Some(t) = DashTab::TravelTimes.transition(ctx, app, &self.panel) {
|
||||
@ -142,9 +149,9 @@ impl State<App> for TravelTimes {
|
||||
filter.modes.insert(m);
|
||||
}
|
||||
}
|
||||
let mut new_panel = TravelTimes::make_panel(ctx, app, filter);
|
||||
new_panel.restore(ctx, &self.panel);
|
||||
self.panel = new_panel;
|
||||
let mut new = TravelTimes::new(ctx, app, filter);
|
||||
new.panel.restore(ctx, &self.panel);
|
||||
*self = new;
|
||||
Transition::Keep
|
||||
}
|
||||
_ => Transition::Keep,
|
||||
@ -462,6 +469,7 @@ fn contingency_table(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
|
||||
))
|
||||
.fg(Color::hex("#72CE36")),
|
||||
]),
|
||||
None,
|
||||
));
|
||||
}
|
||||
if num_loss > 0 {
|
||||
@ -496,6 +504,7 @@ fn contingency_table(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
|
||||
Line(format!("Lost {} in total", total_loss.to_rounded_string(1)))
|
||||
.fg(Color::hex("#EB3223")),
|
||||
]),
|
||||
None,
|
||||
));
|
||||
}
|
||||
x1 += bar_width;
|
||||
|
@ -1,10 +1,11 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use abstutil::{abbreviated_format, prettyprint_usize};
|
||||
use geom::{Angle, Distance, Duration, Line, Polygon, Pt2D, Time};
|
||||
use map_gui::tools::ColorScale;
|
||||
use sim::{Problem, TripMode};
|
||||
use sim::{Problem, TripID, TripMode};
|
||||
use widgetry::{Color, DrawWithTooltips, GeomBatch, GeomBatchStack, StackAlignment, Text, Widget};
|
||||
|
||||
use crate::{App, EventCtx};
|
||||
@ -54,10 +55,15 @@ pub trait TripProblemFilter {
|
||||
fn include_no_changes(&self) -> bool;
|
||||
|
||||
// Returns:
|
||||
// 1) trip duration after changes
|
||||
// 2) difference in number of matching problems, where positive means MORE problems after
|
||||
// 1) trip ID
|
||||
// 2) trip duration after changes
|
||||
// 3) difference in number of matching problems, where positive means MORE problems after
|
||||
// changes
|
||||
fn trip_problems(&self, app: &App, problem_type: ProblemType) -> Vec<(Duration, isize)> {
|
||||
fn trip_problems(
|
||||
&self,
|
||||
app: &App,
|
||||
problem_type: ProblemType,
|
||||
) -> Vec<(TripID, Duration, isize)> {
|
||||
let before = app.prebaked();
|
||||
let after = app.primary.sim.get_analytics();
|
||||
let empty = Vec::new();
|
||||
@ -73,7 +79,7 @@ pub trait TripProblemFilter {
|
||||
if !self.include_no_changes() && count_after == count_before {
|
||||
continue;
|
||||
}
|
||||
points.push((time_after, count_after - count_before));
|
||||
points.push((id, time_after, count_after - count_before));
|
||||
}
|
||||
}
|
||||
points
|
||||
@ -97,9 +103,14 @@ lazy_static::lazy_static! {
|
||||
static ref CLEAR_COLOR_SCALE: ColorScale = ColorScale(vec![Color::CLEAR, Color::CLEAR]);
|
||||
}
|
||||
|
||||
pub fn problem_matrix(ctx: &mut EventCtx, app: &App, trips: &[(Duration, isize)]) -> Widget {
|
||||
let points = trips;
|
||||
|
||||
/// The `title` is just used to generate unique labels. Returns a widget and a mapping from
|
||||
/// `Outcome::Clicked` labels to the list of trips matching the bucket.
|
||||
pub fn problem_matrix(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
title: &str,
|
||||
trips: Vec<(TripID, Duration, isize)>,
|
||||
) -> (Widget, HashMap<String, Vec<TripID>>) {
|
||||
let duration_buckets = vec![
|
||||
Duration::ZERO,
|
||||
Duration::minutes(5),
|
||||
@ -110,14 +121,15 @@ pub fn problem_matrix(ctx: &mut EventCtx, app: &App, trips: &[(Duration, isize)]
|
||||
];
|
||||
|
||||
let num_buckets = 7;
|
||||
let mut matrix = Matrix::new(duration_buckets, bucketize_isizes(num_buckets, points));
|
||||
for (x, y) in points {
|
||||
matrix.add_pt(*x, *y);
|
||||
let mut matrix = Matrix::new(duration_buckets, bucketize_isizes(num_buckets, &trips));
|
||||
for (id, x, y) in trips {
|
||||
matrix.add_pt(id, x, y);
|
||||
}
|
||||
matrix.draw(
|
||||
ctx,
|
||||
app,
|
||||
MatrixOptions {
|
||||
title: title.to_string(),
|
||||
total_width: 600.0,
|
||||
total_height: 600.0,
|
||||
color_scale_for_bucket: Box::new(|app, _, n| match n.cmp(&0) {
|
||||
@ -186,17 +198,17 @@ pub fn problem_matrix(ctx: &mut EventCtx, app: &App, trips: &[(Duration, isize)]
|
||||
)
|
||||
}
|
||||
|
||||
/// Aka a 2D histogram. Counts the number of matching points in each cell.
|
||||
struct Matrix<X, Y> {
|
||||
counts: Vec<usize>,
|
||||
/// Aka a 2D histogram. Tracks matching IDs in each cell.
|
||||
struct Matrix<ID, X, Y> {
|
||||
entries: Vec<Vec<ID>>,
|
||||
buckets_x: Vec<X>,
|
||||
buckets_y: Vec<Y>,
|
||||
}
|
||||
|
||||
impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y> {
|
||||
fn new(buckets_x: Vec<X>, buckets_y: Vec<Y>) -> Matrix<X, Y> {
|
||||
impl<ID, X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<ID, X, Y> {
|
||||
fn new(buckets_x: Vec<X>, buckets_y: Vec<Y>) -> Matrix<ID, X, Y> {
|
||||
Matrix {
|
||||
counts: std::iter::repeat(0)
|
||||
entries: std::iter::repeat_with(Vec::new)
|
||||
.take(buckets_x.len() * buckets_y.len())
|
||||
.collect(),
|
||||
buckets_x,
|
||||
@ -204,7 +216,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pt(&mut self, x: X, y: Y) {
|
||||
fn add_pt(&mut self, id: ID, x: X, y: Y) {
|
||||
// Find its bucket
|
||||
// TODO Unit test this
|
||||
let x_idx = self
|
||||
@ -220,7 +232,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
|
||||
.unwrap_or(self.buckets_y.len())
|
||||
- 1;
|
||||
let idx = self.idx(x_idx, y_idx);
|
||||
self.counts[idx] += 1;
|
||||
self.entries[idx].push(id);
|
||||
}
|
||||
|
||||
fn idx(&self, x: usize, y: usize) -> usize {
|
||||
@ -228,21 +240,32 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
|
||||
y * self.buckets_x.len() + x
|
||||
}
|
||||
|
||||
fn draw(self, ctx: &mut EventCtx, app: &App, opts: MatrixOptions<X, Y>) -> Widget {
|
||||
fn draw(
|
||||
mut self,
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
opts: MatrixOptions<X, Y>,
|
||||
) -> (Widget, HashMap<String, Vec<ID>>) {
|
||||
let mut grid_batch = GeomBatch::new();
|
||||
let mut tooltips = Vec::new();
|
||||
let cell_width = opts.total_width / (self.buckets_x.len() as f64);
|
||||
let cell_height = opts.total_height / (self.buckets_y.len() as f64);
|
||||
let cell = Polygon::rectangle(cell_width, cell_height);
|
||||
|
||||
let max_count = *self.counts.iter().max().unwrap() as f64;
|
||||
let max_count = self.entries.iter().map(|list| list.len()).max().unwrap() as f64;
|
||||
|
||||
let mut mapping = HashMap::new();
|
||||
for x in 0..self.buckets_x.len() - 1 {
|
||||
for y in 0..self.buckets_y.len() - 1 {
|
||||
let is_first_xbucket = x == 0;
|
||||
let is_last_xbucket = x == self.buckets_x.len() - 2;
|
||||
let is_middle_ybucket = y + 1 == self.buckets_y.len() / 2;
|
||||
let count = self.counts[self.idx(x, y)];
|
||||
let idx = self.idx(x, y);
|
||||
let count = self.entries[idx].len();
|
||||
let bucket_label = format!("{}/{}", opts.title, idx);
|
||||
if count != 0 {
|
||||
mapping.insert(bucket_label.clone(), std::mem::take(&mut self.entries[idx]));
|
||||
}
|
||||
let color = if count == 0 {
|
||||
widgetry::Color::CLEAR
|
||||
} else {
|
||||
@ -288,6 +311,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
|
||||
(self.buckets_y[y], self.buckets_y[y + 1]),
|
||||
count,
|
||||
),
|
||||
Some(bucket_label),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -388,7 +412,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
|
||||
x_axis_scale
|
||||
};
|
||||
|
||||
for (polygon, _) in tooltips.iter_mut() {
|
||||
for (polygon, _, _) in &mut tooltips {
|
||||
let mut translated = polygon.translate(y_axis_batch.get_bounds().width(), 0.0);
|
||||
std::mem::swap(&mut translated, polygon);
|
||||
}
|
||||
@ -398,11 +422,21 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
|
||||
let mut chart = GeomBatchStack::horizontal(vec![y_axis_batch, col.batch()]);
|
||||
chart.set_alignment(StackAlignment::Top);
|
||||
|
||||
DrawWithTooltips::new_widget(ctx, chart.batch(), tooltips, Box::new(|_| GeomBatch::new()))
|
||||
(
|
||||
DrawWithTooltips::new_widget(
|
||||
ctx,
|
||||
chart.batch(),
|
||||
tooltips,
|
||||
Box::new(|_| GeomBatch::new()),
|
||||
),
|
||||
mapping,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct MatrixOptions<X, Y> {
|
||||
// To disambiguate labels
|
||||
title: String,
|
||||
total_width: f64,
|
||||
total_height: f64,
|
||||
// (lower_bound, upper_bound) -> Cell Label
|
||||
@ -411,7 +445,7 @@ struct MatrixOptions<X, Y> {
|
||||
tooltip_for_bucket: Box<dyn Fn((Option<X>, Option<X>), (Y, Y), usize) -> Text>,
|
||||
}
|
||||
|
||||
fn bucketize_isizes(max_buckets: usize, pts: &[(Duration, isize)]) -> Vec<isize> {
|
||||
fn bucketize_isizes(max_buckets: usize, pts: &[(TripID, Duration, isize)]) -> Vec<isize> {
|
||||
debug_assert!(
|
||||
max_buckets % 2 == 1,
|
||||
"num_buckets must be odd to have a symmetrical number of buckets around axis"
|
||||
@ -420,8 +454,8 @@ fn bucketize_isizes(max_buckets: usize, pts: &[(Duration, isize)]) -> Vec<isize>
|
||||
|
||||
let positive_buckets = (max_buckets - 1) / 2;
|
||||
// uniformly sized integer buckets
|
||||
let max = match pts.iter().max_by_key(|(_, cnt)| cnt.abs()) {
|
||||
Some(t) if (t.1.abs() as usize) >= positive_buckets => t.1.abs(),
|
||||
let max = match pts.iter().max_by_key(|(_, _, cnt)| cnt.abs()) {
|
||||
Some(t) if (t.2.abs() as usize) >= positive_buckets => t.2.abs(),
|
||||
_ => {
|
||||
// Enforce a bucket width of at least 1.
|
||||
let negative_buckets = -(positive_buckets as isize);
|
||||
|
@ -4,8 +4,9 @@ use map_gui::tools::PopupMsg;
|
||||
use map_gui::ID;
|
||||
use sim::AlertLocation;
|
||||
use widgetry::{
|
||||
Choice, Color, ControlState, EdgeInsets, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
|
||||
Line, Outcome, Panel, PersistentSplit, ScreenDims, Text, TextExt, VerticalAlignment, Widget,
|
||||
Choice, Color, ControlState, DrawWithTooltips, EdgeInsets, EventCtx, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, Key, Line, Outcome, Panel, PersistentSplit, ScreenDims, Text, TextExt,
|
||||
VerticalAlignment, Widget,
|
||||
};
|
||||
|
||||
use crate::app::{App, Transition};
|
||||
@ -253,7 +254,6 @@ impl TimePanel {
|
||||
progress_bar.push(cursor_fg, triangle_poly);
|
||||
}
|
||||
|
||||
use widgetry::DrawWithTooltips;
|
||||
let mut tooltip_text = Text::from("Finished Trips");
|
||||
tooltip_text.add_line(format!(
|
||||
"{} ({}% of total)",
|
||||
@ -286,7 +286,7 @@ impl TimePanel {
|
||||
|
||||
let bounds = progress_bar.get_bounds();
|
||||
let bounding_box = Polygon::rectangle(bounds.width(), bounds.height());
|
||||
let tooltip = vec![(bounding_box, tooltip_text)];
|
||||
let tooltip = vec![(bounding_box, tooltip_text, None)];
|
||||
DrawWithTooltips::new_widget(ctx, progress_bar, tooltip, Box::new(|_| GeomBatch::new()))
|
||||
}
|
||||
|
||||
|
@ -293,7 +293,7 @@ impl<'a, 'c> Image<'a, 'c> {
|
||||
DrawWithTooltips::new_widget(
|
||||
ctx,
|
||||
batch,
|
||||
vec![(bounds.get_rectangle(), tooltip)],
|
||||
vec![(bounds.get_rectangle(), tooltip, None)],
|
||||
Box::new(|_| GeomBatch::new()),
|
||||
)
|
||||
} else {
|
||||
|
@ -1,8 +1,8 @@
|
||||
use geom::Polygon;
|
||||
|
||||
use crate::{
|
||||
Drawable, EventCtx, GeomBatch, GfxCtx, ScreenDims, ScreenPt, ScreenRectangle, Text, Widget,
|
||||
WidgetImpl, WidgetOutput,
|
||||
Drawable, EventCtx, GeomBatch, GfxCtx, Outcome, ScreenDims, ScreenPt, ScreenRectangle, Text,
|
||||
Widget, WidgetImpl, WidgetOutput,
|
||||
};
|
||||
|
||||
// Just draw something, no interaction.
|
||||
@ -41,8 +41,9 @@ impl WidgetImpl for JustDraw {
|
||||
|
||||
pub struct DrawWithTooltips {
|
||||
draw: Drawable,
|
||||
tooltips: Vec<(Polygon, Text)>,
|
||||
tooltips: Vec<(Polygon, Text, Option<String>)>,
|
||||
hover: Box<dyn Fn(&Polygon) -> GeomBatch>,
|
||||
hovering_on_idx: Option<usize>,
|
||||
|
||||
top_left: ScreenPt,
|
||||
dims: ScreenDims,
|
||||
@ -50,20 +51,23 @@ pub struct DrawWithTooltips {
|
||||
|
||||
impl DrawWithTooltips {
|
||||
/// `batch`: the `GeomBatch` to draw
|
||||
/// `tooltips`: (hitbox, text) tuples where each `text` is shown when the user hovers over
|
||||
/// the respective `hitbox`
|
||||
/// `tooltips`: (hitbox, text, clickable label) tuples where each `text` is shown when the user hovers over
|
||||
/// the respective `hitbox`. If a label is present and the user clicks the
|
||||
/// `hitbox`, then it acts like a button click. It's assumed the hitboxes are
|
||||
/// non-overlapping.
|
||||
/// `hover`: returns a GeomBatch to render upon hovering. Return an `GeomBox::new()` if
|
||||
/// you want hovering to be a no-op
|
||||
pub fn new_widget(
|
||||
ctx: &EventCtx,
|
||||
batch: GeomBatch,
|
||||
tooltips: Vec<(Polygon, Text)>,
|
||||
tooltips: Vec<(Polygon, Text, Option<String>)>,
|
||||
hover: Box<dyn Fn(&Polygon) -> GeomBatch>,
|
||||
) -> Widget {
|
||||
Widget::new(Box::new(DrawWithTooltips {
|
||||
dims: batch.get_dims(),
|
||||
top_left: ScreenPt::new(0.0, 0.0),
|
||||
hover,
|
||||
hovering_on_idx: None,
|
||||
draw: ctx.upload(batch),
|
||||
tooltips,
|
||||
}))
|
||||
@ -79,26 +83,40 @@ impl WidgetImpl for DrawWithTooltips {
|
||||
self.top_left = top_left;
|
||||
}
|
||||
|
||||
fn event(&mut self, _: &mut EventCtx, _: &mut WidgetOutput) {}
|
||||
fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
|
||||
if ctx.redo_mouseover() {
|
||||
self.hovering_on_idx = 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();
|
||||
for (idx, (hitbox, _, _)) in self.tooltips.iter().enumerate() {
|
||||
if hitbox.contains_pt(translated) {
|
||||
self.hovering_on_idx = Some(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = self.hovering_on_idx {
|
||||
if ctx.normal_left_click() {
|
||||
if let Some(ref label) = self.tooltips[idx].2 {
|
||||
output.outcome = Outcome::Clicked(label.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx) {
|
||||
g.redraw_at(self.top_left, &self.draw);
|
||||
|
||||
if let Some(cursor) = g.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 (region, txt) in &self.tooltips {
|
||||
if region.contains_pt(translated) {
|
||||
let extra = g.upload((self.hover)(region));
|
||||
g.redraw_at(self.top_left, &extra);
|
||||
g.draw_mouse_tooltip(txt.clone());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(idx) = self.hovering_on_idx {
|
||||
let (hitbox, txt, _) = &self.tooltips[idx];
|
||||
let extra = g.upload((self.hover)(hitbox));
|
||||
g.redraw_at(self.top_left, &extra);
|
||||
g.draw_mouse_tooltip(txt.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user