diff --git a/ezgui/src/input.rs b/ezgui/src/input.rs index d5e9a039fe..3c54aaaaf5 100644 --- a/ezgui/src/input.rs +++ b/ezgui/src/input.rs @@ -305,9 +305,11 @@ impl UserInput { pub fn populate_osd(&mut self, osd: &mut Text) { for (key, a) in self.important_actions.drain(..) { - osd.add(Line("Press ")); - osd.append(Line(key.describe()).fg(text::HOTKEY_COLOR)); - osd.append(Line(format!(" to {}", a))); + osd.add_appended(vec![ + Line("Press "), + Line(key.describe()).fg(text::HOTKEY_COLOR), + Line(format!(" to {}", a)), + ]); } } diff --git a/ezgui/src/text.rs b/ezgui/src/text.rs index e08544c8bb..3d5d13eecc 100644 --- a/ezgui/src/text.rs +++ b/ezgui/src/text.rs @@ -120,6 +120,23 @@ impl Text { self.lines.last_mut().unwrap().1.push(line); } + pub fn add_appended(&mut self, lines: Vec) { + assert!(lines.len() > 1); + for (idx, l) in lines.into_iter().enumerate() { + if idx == 0 { + self.add(l); + } else { + self.append(l); + } + } + } + + pub fn append_all(&mut self, lines: Vec) { + for l in lines { + self.append(l); + } + } + // TODO Ideally we'd wrap last-minute when drawing, but eh, start somewhere. pub fn add_wrapped_line(&mut self, canvas: &Canvas, line: String) { let wrap_to = canvas.window_width / MAX_CHAR_WIDTH; diff --git a/ezgui/src/widgets/autocomplete.rs b/ezgui/src/widgets/autocomplete.rs index 07d38cb2ac..440190f2f3 100644 --- a/ezgui/src/widgets/autocomplete.rs +++ b/ezgui/src/widgets/autocomplete.rs @@ -59,9 +59,11 @@ impl Autocomplete { txt.add(Line(&self.line[0..self.cursor_x])); if self.cursor_x < self.line.len() { // TODO This "cursor" looks awful! - txt.append(Line("|").fg(text::SELECTED_COLOR)); - txt.append(Line(&self.line[self.cursor_x..=self.cursor_x])); - txt.append(Line(&self.line[self.cursor_x + 1..])); + txt.append_all(vec![ + Line("|").fg(text::SELECTED_COLOR), + Line(&self.line[self.cursor_x..=self.cursor_x]), + Line(&self.line[self.cursor_x + 1..]), + ]); } else { txt.append(Line("|").fg(text::SELECTED_COLOR)); } diff --git a/ezgui/src/widgets/menu.rs b/ezgui/src/widgets/menu.rs index 94425c0deb..1fb5642f3f 100644 --- a/ezgui/src/widgets/menu.rs +++ b/ezgui/src/widgets/menu.rs @@ -338,8 +338,10 @@ impl Menu { txt.override_width = Some(self.total_width); if choice.active { if let Some(key) = choice.hotkey { - txt.add(Line(key.describe()).fg(text::HOTKEY_COLOR)); - txt.append(Line(format!(" - {}", choice.label))); + txt.add_appended(vec![ + Line(key.describe()).fg(text::HOTKEY_COLOR), + Line(format!(" - {}", choice.label)), + ]); } else { txt.add(Line(&choice.label)); } diff --git a/ezgui/src/widgets/text_box.rs b/ezgui/src/widgets/text_box.rs index 8a3d9b5a24..fc56bbadf8 100644 --- a/ezgui/src/widgets/text_box.rs +++ b/ezgui/src/widgets/text_box.rs @@ -26,9 +26,11 @@ impl TextBox { txt.add(Line(&self.line[0..self.cursor_x])); if self.cursor_x < self.line.len() { // TODO This "cursor" looks awful! - txt.append(Line("|").fg(text::SELECTED_COLOR)); - txt.append(Line(&self.line[self.cursor_x..=self.cursor_x])); - txt.append(Line(&self.line[self.cursor_x + 1..])); + txt.append_all(vec![ + Line("|").fg(text::SELECTED_COLOR), + Line(&self.line[self.cursor_x..=self.cursor_x]), + Line(&self.line[self.cursor_x + 1..]), + ]); } else { txt.append(Line("|").fg(text::SELECTED_COLOR)); } diff --git a/fix_map_geom/src/main.rs b/fix_map_geom/src/main.rs index a6c83eb014..614129ab11 100644 --- a/fix_map_geom/src/main.rs +++ b/fix_map_geom/src/main.rs @@ -82,32 +82,38 @@ impl GUI for UI { let len = self.hints.hints.len(); let mut txt = Text::prompt("Fix Map Geometry"); - txt.add(Line(len.to_string()).fg(Color::CYAN)); - txt.append(Line(" hints, ")); - txt.append( + txt.add_appended(vec![ + Line(len.to_string()).fg(Color::CYAN), + Line(" hints, "), Line(self.hints.parking_overrides.len().to_string()).fg(Color::CYAN), - ); - txt.append(Line(" parking overrides")); + Line(" parking overrides"), + ]); if let Some(ID::Road(r)) = self.world.get_selection() { - txt.add(Line(r.to_string()).fg(Color::RED)); - txt.append(Line(format!( - " is {} long", - self.data.roads[&r].trimmed_center_pts.length() - ))); + txt.add_appended(vec![ + Line(r.to_string()).fg(Color::RED), + Line(format!( + " is {} long", + self.data.roads[&r].trimmed_center_pts.length() + )), + ]); if self.data.roads[&r].has_parking() { txt.add(Line("Has parking")); } else { txt.add(Line("No parking")); } for (k, v) in &self.raw.roads[&r].osm_tags { - txt.add(Line(k).fg(Color::RED)); - txt.append(Line(" = ")); - txt.append(Line(v).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(k).fg(Color::RED), + Line(" = "), + Line(v).fg(Color::CYAN), + ]); } } if let Some(ID::Intersection(i)) = self.world.get_selection() { - txt.add(Line(i.to_string()).fg(Color::RED)); - txt.append(Line(" OSM tag diffs:")); + txt.add_appended(vec![ + Line(i.to_string()).fg(Color::RED), + Line(" OSM tag diffs:"), + ]); let roads = &self.data.intersections[&i].roads; if roads.len() == 2 { let mut iter = roads.iter(); @@ -117,27 +123,33 @@ impl GUI for UI { for (k, v1) in r1_tags { if let Some(v2) = r2_tags.get(k) { if v1 != v2 { - txt.add(Line(k).fg(Color::RED)); - txt.append(Line(" = ")); - txt.append(Line(v1).fg(Color::CYAN)); - txt.append(Line(" / ")); - txt.append(Line(v2).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(k).fg(Color::RED), + Line(" = "), + Line(v1).fg(Color::CYAN), + Line(" / "), + Line(v2).fg(Color::CYAN), + ]); } } else { - txt.add(Line(k).fg(Color::RED)); - txt.append(Line(" = ")); - txt.append(Line(v1).fg(Color::CYAN)); - txt.append(Line(" / ")); - txt.append(Line("MISSING").fg(Color::CYAN)); + txt.add_appended(vec![ + Line(k).fg(Color::RED), + Line(" = "), + Line(v1).fg(Color::CYAN), + Line(" / "), + Line("MISSING").fg(Color::CYAN), + ]); } } for (k, v2) in r2_tags { if !r1_tags.contains_key(k) { - txt.add(Line(k).fg(Color::RED)); - txt.append(Line(" = ")); - txt.append(Line("MISSING").fg(Color::CYAN)); - txt.append(Line(" / ")); - txt.append(Line(v2).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(k).fg(Color::RED), + Line(" = "), + Line("MISSING").fg(Color::CYAN), + Line(" / "), + Line(v2).fg(Color::CYAN), + ]); } } } diff --git a/game/src/abtest/score.rs b/game/src/abtest/score.rs index 866416d13b..496862f97e 100644 --- a/game/src/abtest/score.rs +++ b/game/src/abtest/score.rs @@ -30,17 +30,21 @@ impl Scoreboard { let t2 = secondary.sim.get_finished_trips(); let mut summary = Text::new(); - summary.add(Line("Score at ")); - summary.append(Line(primary.sim.time().to_string()).fg(Color::RED)); - summary.append(Line(format!( - "... {} / {}", - primary.map.get_edits().edits_name, - secondary.map.get_edits().edits_name - ))); - summary.add(Line(prettyprint_usize(t1.unfinished_trips)).fg(Color::CYAN)); - summary.append(Line(" | ")); - summary.append(Line(prettyprint_usize(t2.unfinished_trips)).fg(Color::RED)); - summary.append(Line(" unfinished trips")); + summary.add_appended(vec![ + Line("Score at "), + Line(primary.sim.time().to_string()).fg(Color::RED), + Line(format!( + "... {} / {}", + primary.map.get_edits().edits_name, + secondary.map.get_edits().edits_name + )), + ]); + summary.add_appended(vec![ + Line(prettyprint_usize(t1.unfinished_trips)).fg(Color::CYAN), + Line(" | "), + Line(prettyprint_usize(t2.unfinished_trips)).fg(Color::RED), + Line(" unfinished trips"), + ]); let cmp = CompareTrips::new(t1, t2); for (mode, trips) in &cmp @@ -64,29 +68,33 @@ impl Scoreboard { deltas.sort(); let len = deltas.len() as f64; - summary.add(Line(format!("{:?}", mode)).fg(Color::CYAN)); - summary.append(Line(format!( - " trips: {} same, {} different", - abstutil::prettyprint_usize(num_same), - abstutil::prettyprint_usize(deltas.len()) - ))); + summary.add_appended(vec![ + Line(format!("{:?}", mode)).fg(Color::CYAN), + Line(format!( + " trips: {} same, {} different", + abstutil::prettyprint_usize(num_same), + abstutil::prettyprint_usize(deltas.len()) + )), + ]); if !deltas.is_empty() { - summary.add(Line(" deltas: ")); - summary.append(Line("50%ile").fg(Color::RED)); - summary.append(Line(format!( - " {}, ", - handle_negative(deltas[(0.5 * len).floor() as usize]) - ))); - summary.append(Line("90%ile").fg(Color::RED)); - summary.append(Line(format!( - " {}, ", - handle_negative(deltas[(0.9 * len).floor() as usize]) - ))); - summary.append(Line("99%ile").fg(Color::RED)); - summary.append(Line(format!( - " {}", - handle_negative(deltas[(0.99 * len).floor() as usize]) - ))); + summary.add_appended(vec![ + Line(" deltas: "), + Line("50%ile").fg(Color::RED), + Line(format!( + " {}, ", + handle_negative(deltas[(0.5 * len).floor() as usize]) + )), + Line("90%ile").fg(Color::RED), + Line(format!( + " {}, ", + handle_negative(deltas[(0.9 * len).floor() as usize]) + )), + Line("99%ile").fg(Color::RED), + Line(format!( + " {}", + handle_negative(deltas[(0.99 * len).floor() as usize]) + )), + ]); } } diff --git a/game/src/common/mod.rs b/game/src/common/mod.rs index bfb2b3657a..340b3387ee 100644 --- a/game/src/common/mod.rs +++ b/game/src/common/mod.rs @@ -88,15 +88,19 @@ impl CommonState { osd.append(Line("...")); } Some(ID::Lane(l)) => { - osd.append(Line(l.to_string()).fg(id_color)); - osd.append(Line(" is ")); - osd.append(Line(map.get_parent(*l).get_name()).fg(name_color)); + osd.append_all(vec![ + Line(l.to_string()).fg(id_color), + Line(" is "), + Line(map.get_parent(*l).get_name()).fg(name_color), + ]); } Some(ID::Building(b)) => { let bldg = map.get_b(*b); - osd.append(Line(b.to_string()).fg(id_color)); - osd.append(Line(" is ")); - osd.append(Line(bldg.get_name()).fg(name_color)); + osd.append_all(vec![ + Line(b.to_string()).fg(id_color), + Line(" is "), + Line(bldg.get_name()).fg(name_color), + ]); if let Some(ref p) = bldg.parking { osd.append(Line(format!( " ({} parking spots via {})", @@ -105,15 +109,16 @@ impl CommonState { } } Some(ID::Turn(t)) => { - osd.append(Line(format!("TurnID({})", map.get_t(*t).lookup_idx)).fg(id_color)); - osd.append(Line(" between ")); - osd.append(Line(map.get_parent(t.src).get_name()).fg(name_color)); - osd.append(Line(" and ")); - osd.append(Line(map.get_parent(t.dst).get_name()).fg(name_color)); + osd.append_all(vec![ + Line(format!("TurnID({})", map.get_t(*t).lookup_idx)).fg(id_color), + Line(" between "), + Line(map.get_parent(t.src).get_name()).fg(name_color), + Line(" and "), + Line(map.get_parent(t.dst).get_name()).fg(name_color), + ]); } Some(ID::Intersection(i)) => { - osd.append(Line(i.to_string()).fg(id_color)); - osd.append(Line(" of ")); + osd.append_all(vec![Line(i.to_string()).fg(id_color), Line(" of ")]); let mut road_names = BTreeSet::new(); for r in &map.get_i(*i).roads { @@ -130,13 +135,14 @@ impl CommonState { Some(ID::Car(c)) => { osd.append(Line(c.to_string()).fg(id_color)); if let Some(r) = ui.primary.sim.bus_route_id(*c) { - osd.append(Line(" serving ")); - osd.append(Line(&map.get_br(r).name).fg(name_color)); + osd.append_all(vec![ + Line(" serving "), + Line(&map.get_br(r).name).fg(name_color), + ]); } } Some(ID::BusStop(bs)) => { - osd.append(Line(bs.to_string()).fg(id_color)); - osd.append(Line(" serving ")); + osd.append_all(vec![Line(bs.to_string()).fg(id_color), Line(" serving ")]); let routes = map.get_routes_serving_stop(*bs); let len = routes.len(); diff --git a/game/src/debug/objects.rs b/game/src/debug/objects.rs index 978e69aa5f..e588aea0f0 100644 --- a/game/src/debug/objects.rs +++ b/game/src/debug/objects.rs @@ -149,16 +149,20 @@ fn tooltip_lines(id: ID, g: &mut GfxCtx, ctx: &PerMapUI) -> Text { match id { ID::Road(id) => { let r = map.get_r(id); - txt.add(Line(format!("{} (originally {}) is ", r.id, r.stable_id))); - txt.append(Line(r.get_name()).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(format!("{} (originally {}) is ", r.id, r.stable_id)), + Line(r.get_name()).fg(Color::CYAN), + ]); txt.add(Line(format!("From OSM way {}", r.osm_way_id))); } ID::Lane(id) => { let l = map.get_l(id); let r = map.get_r(l.parent); - txt.add(Line(format!("{} is ", l.id))); - txt.append(Line(r.get_name()).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(format!("{} is ", l.id)), + Line(r.get_name()).fg(Color::CYAN), + ]); txt.add(Line(format!("From OSM way {}", r.osm_way_id))); txt.add(Line(format!( "Parent {} (originally {}) points to {}", @@ -258,8 +262,10 @@ fn tooltip_lines(id: ID, g: &mut GfxCtx, ctx: &PerMapUI) -> Text { fn styled_kv(txt: &mut Text, tags: &BTreeMap) { for (k, v) in tags { - txt.add(Line(k).fg(Color::RED)); - txt.append(Line(" = ")); - txt.append(Line(v).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(k).fg(Color::RED), + Line(" = "), + Line(v).fg(Color::CYAN), + ]); } } diff --git a/game/src/edit/stop_signs.rs b/game/src/edit/stop_signs.rs index 83c0909b28..ebb8d7821d 100644 --- a/game/src/edit/stop_signs.rs +++ b/game/src/edit/stop_signs.rs @@ -120,8 +120,10 @@ impl State for StopSignEditor { self.menu.draw(g); if let Some(r) = self.selected_sign { let mut osd = Text::new(); - osd.add(Line("Stop sign for ")); - osd.append(Line(ui.primary.map.get_r(r).get_name()).fg(ui.cs.get("OSD name color"))); + osd.add_appended(vec![ + Line("Stop sign for "), + Line(ui.primary.map.get_r(r).get_name()).fg(ui.cs.get("OSD name color")), + ]); CommonState::draw_custom_osd(g, osd); } else { CommonState::draw_osd(g, ui, &None); diff --git a/game/src/mission/dataviz.rs b/game/src/mission/dataviz.rs index a0cf859052..f7caa747b3 100644 --- a/game/src/mission/dataviz.rs +++ b/game/src/mission/dataviz.rs @@ -66,8 +66,10 @@ impl State for DataVisualizer { fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition { let mut txt = Text::prompt("Data Visualizer"); if let Some(ref name) = self.current_tract { - txt.add(Line("Census ")); - txt.append(Line(name).fg(ui.cs.get("OSD name color"))); + txt.add_appended(vec![ + Line("Census "), + Line(name).fg(ui.cs.get("OSD name color")), + ]); let tract = &self.tracts[name]; txt.add(Line(format!( "{} buildings", @@ -126,8 +128,10 @@ impl State for DataVisualizer { self.menu.draw(g); if let Some(ref name) = self.current_tract { let mut osd = Text::new(); - osd.add(Line("Census ")); - osd.append(Line(name).fg(ui.cs.get("OSD name color"))); + osd.add_appended(vec![ + Line("Census "), + Line(name).fg(ui.cs.get("OSD name color")), + ]); CommonState::draw_custom_osd(g, osd); } else { CommonState::draw_osd(g, ui, &None); @@ -150,9 +154,11 @@ impl State for DataVisualizer { } else { let mut txt = Text::new(); for (k, v) in kv { - txt.add(Line(k).fg(Color::RED)); - txt.append(Line(" = ")); - txt.append(Line(v.to_string()).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(k).fg(Color::RED), + Line(" = "), + Line(v.to_string()).fg(Color::CYAN), + ]); } g.draw_blocking_text(&txt, (HorizontalAlignment::Left, VerticalAlignment::Top)); } @@ -236,15 +242,15 @@ fn bar_chart(g: &mut GfxCtx, data: &BTreeMap) { if name == "Total:" { continue; } - labels.add(Line(format!("{} (", name)).size(40)); - labels.append( + labels.add_appended(vec![ + Line(format!("{} (", name)).size(40), Line(format!( "{}%", ((est.value as f64) / (sum as f64) * 100.0) as usize )) .fg(Color::RED), - ); - labels.append(Line(")")); + Line(")"), + ]); } let (txt_width, total_height) = g.text_dims(&labels); let line_height = total_height / ((data.len() as f64) - 1.0); diff --git a/game/src/mission/individ_trips.rs b/game/src/mission/individ_trips.rs index ea49d9b88b..5bf576b087 100644 --- a/game/src/mission/individ_trips.rs +++ b/game/src/mission/individ_trips.rs @@ -108,9 +108,11 @@ impl State for TripsVisualizer { self.slider.draw(g); if let Some(ID::Building(b)) = ui.primary.current_selection { let mut osd = Text::new(); - osd.add(Line(b.to_string()).fg(ui.cs.get("OSD ID color"))); - osd.append(Line(" is ")); - osd.append(Line(ui.primary.map.get_b(b).get_name()).fg(ui.cs.get("OSD name color"))); + osd.add_appended(vec![ + Line(b.to_string()).fg(ui.cs.get("OSD ID color")), + Line(" is "), + Line(ui.primary.map.get_b(b).get_name()).fg(ui.cs.get("OSD name color")), + ]); if let Some(md) = self.bldgs.get(&b) { osd.append(Line(format!( ". {} households, {} employees, {} offstreet parking spaces", diff --git a/game/src/mission/scenario.rs b/game/src/mission/scenario.rs index 57f52dd3e3..1ebb60e092 100644 --- a/game/src/mission/scenario.rs +++ b/game/src/mission/scenario.rs @@ -213,17 +213,19 @@ impl State for ScenarioManager { if let Some(ID::Building(b)) = ui.primary.current_selection { let mut osd = Text::new(); - osd.add(Line(b.to_string()).fg(ui.cs.get("OSD ID color"))); - osd.append(Line(" is ")); - osd.append(Line(ui.primary.map.get_b(b).get_name()).fg(ui.cs.get("OSD name color"))); let from = self.trips_from_bldg.get(b); let to = self.trips_to_bldg.get(b); - osd.append(Line(format!( - ". {} trips from here, {} trips to here, {} parked cars needed", - from.len(), - to.len(), - self.cars_needed_per_bldg[&b] - ))); + osd.add_appended(vec![ + Line(b.to_string()).fg(ui.cs.get("OSD ID color")), + Line(" is "), + Line(ui.primary.map.get_b(b).get_name()).fg(ui.cs.get("OSD name color")), + Line(format!( + ". {} trips from here, {} trips to here, {} parked cars needed", + from.len(), + to.len(), + self.cars_needed_per_bldg[&b] + )), + ]); CommonState::draw_custom_osd(g, osd); } else { CommonState::draw_osd(g, ui, &ui.primary.current_selection); diff --git a/game/src/sandbox/score.rs b/game/src/sandbox/score.rs index 210e7bacb5..ae68398e6f 100644 --- a/game/src/sandbox/score.rs +++ b/game/src/sandbox/score.rs @@ -28,10 +28,14 @@ impl Scoreboard { let t = ui.primary.sim.get_finished_trips(); let mut summary = Text::new(); - summary.add(Line("Score at ")); - summary.append(Line(ui.primary.sim.time().to_string()).fg(Color::RED)); - summary.add(Line(prettyprint_usize(t.unfinished_trips)).fg(Color::CYAN)); - summary.append(Line(" unfinished trips")); + summary.add_appended(vec![ + Line("Score at "), + Line(ui.primary.sim.time().to_string()).fg(Color::RED), + ]); + summary.add_appended(vec![ + Line(prettyprint_usize(t.unfinished_trips)).fg(Color::CYAN), + Line(" unfinished trips"), + ]); for (mode, trips) in &t .finished_trips @@ -43,8 +47,10 @@ impl Scoreboard { for (_, _, dt) in trips { distrib.add(dt); } - summary.add(Line(format!("{:?}", mode)).fg(Color::CYAN)); - summary.append(Line(format!(" trips: {}", distrib.describe()))); + summary.add_appended(vec![ + Line(format!("{:?}", mode)).fg(Color::CYAN), + Line(format!(" trips: {}", distrib.describe())), + ]); } Scoreboard { menu, summary } diff --git a/synthetic/src/main.rs b/synthetic/src/main.rs index 3dfe5f17ac..7949bab79e 100644 --- a/synthetic/src/main.rs +++ b/synthetic/src/main.rs @@ -213,9 +213,11 @@ impl GUI for UI { if let Some(ID::Lane(id, _, _)) = self.model.get_selection() { let mut txt = Text::new(); for (k, v) in self.model.get_tags(id) { - txt.add(Line(k).fg(Color::RED)); - txt.append(Line(" = ")); - txt.append(Line(v).fg(Color::CYAN)); + txt.add_appended(vec![ + Line(k).fg(Color::RED), + Line(" = "), + Line(v).fg(Color::CYAN), + ]); } g.draw_blocking_text( &txt,