1use geom::{Angle, ArrowCap, Distance, PolyLine, Pt2D};
2use map_gui::tools::DrawSimpleRoadLabels;
3use map_model::FilterType;
4use osm2streets::Direction;
5use widgetry::mapspace::{DummyID, World};
6use widgetry::tools::{ChooseSomething, PopupMsg};
7use widgetry::{
8 lctrl, Choice, Color, ControlState, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx, Key,
9 Line, Outcome, Panel, RewriteColor, State, Text, TextExt, Widget,
10};
11
12use super::{EditMode, EditNeighbourhood, EditOutcome};
13use crate::components::{AppwidePanel, BottomPanel, Mode};
14use crate::logic::AutoFilterHeuristic;
15use crate::render::colors;
16use crate::{is_private, pages, render, App, Neighbourhood, NeighbourhoodID, Transition};
17
18pub struct DesignLTN {
19 appwide_panel: AppwidePanel,
20 bottom_panel: Panel,
21 neighbourhood: Neighbourhood,
22 draw_top_layer: Drawable,
23 draw_under_roads_layer: Drawable,
24 fade_irrelevant: Drawable,
25 labels: DrawSimpleRoadLabels,
26 highlight_cell: World<DummyID>,
27 edit: EditNeighbourhood,
28 preserve_state: crate::save::PreserveState,
30
31 show_unreachable_cell: Drawable,
32 show_suspicious_perimeters: Drawable,
33}
34
35impl DesignLTN {
36 pub fn new_state(
37 ctx: &mut EventCtx,
38 app: &mut App,
39 id: NeighbourhoodID,
40 ) -> Box<dyn State<App>> {
41 app.per_map.current_neighbourhood = Some(id);
42
43 let neighbourhood = Neighbourhood::new(app, id);
44 let fade_irrelevant = neighbourhood.fade_irrelevant(ctx, app);
45
46 let mut label_roads = neighbourhood.perimeter_roads.clone();
47 label_roads.extend(neighbourhood.interior_roads.clone());
48 let labels = DrawSimpleRoadLabels::new(
49 ctx,
50 app,
51 colors::LOCAL_ROAD_LABEL,
52 Box::new(move |r| label_roads.contains(&r.id)),
53 );
54
55 let mut show_suspicious_perimeters = GeomBatch::new();
56 for r in &neighbourhood.suspicious_perimeter_roads {
57 show_suspicious_perimeters
58 .push(Color::RED, app.per_map.map.get_r(*r).get_thick_polygon());
59 }
60
61 let mut state = Self {
62 appwide_panel: AppwidePanel::new(ctx, app, Mode::ModifyNeighbourhood),
63 bottom_panel: Panel::empty(ctx),
64 neighbourhood,
65 draw_top_layer: Drawable::empty(ctx),
66 draw_under_roads_layer: Drawable::empty(ctx),
67 fade_irrelevant,
68 labels,
69 highlight_cell: World::new(),
70 edit: EditNeighbourhood::temporary(),
71 preserve_state: crate::save::PreserveState::DesignLTN(
72 app.partitioning().neighbourhood_to_blocks(id),
73 ),
74
75 show_unreachable_cell: Drawable::empty(ctx),
76 show_suspicious_perimeters: ctx.upload(show_suspicious_perimeters),
77 };
78 state.update(ctx, app);
79 Box::new(state)
80 }
81
82 fn update(&mut self, ctx: &mut EventCtx, app: &App) {
83 let (edit, draw_top_layer, draw_under_roads_layer, render_cells, highlight_cell) =
84 setup_editing(ctx, app, &self.neighbourhood, &self.labels);
85 self.edit = edit;
86 self.draw_top_layer = draw_top_layer;
87 self.draw_under_roads_layer = draw_under_roads_layer;
88 self.highlight_cell = highlight_cell;
89
90 let mut show_unreachable_cell = GeomBatch::new();
91 let mut disconnected_cells = 0;
92 for (idx, cell) in self.neighbourhood.cells.iter().enumerate() {
93 if cell.is_disconnected() {
94 disconnected_cells += 1;
95 show_unreachable_cell.extend(
96 Color::RED.alpha(0.8),
97 render_cells.polygons_per_cell[idx].clone(),
98 );
99 }
100 }
101 let warning1 = if disconnected_cells == 0 {
102 Widget::nothing()
103 } else {
104 let msg = if disconnected_cells == 1 {
105 "1 cell isn't reachable".to_string()
106 } else {
107 format!("{disconnected_cells} cells aren't reachable")
108 };
109
110 ctx.style()
111 .btn_plain
112 .icon_text("system/assets/tools/warning.svg", msg)
113 .label_color(Color::RED, ControlState::Default)
114 .no_tooltip()
115 .build_widget(ctx, "warning1")
116 };
117 self.show_unreachable_cell = ctx.upload(show_unreachable_cell);
118
119 let warning2 = if self.neighbourhood.suspicious_perimeter_roads.is_empty() {
120 Widget::nothing()
121 } else {
122 ctx.style()
123 .btn_plain
124 .icon_text(
125 "system/assets/tools/warning.svg",
126 "Part of the perimeter is a local street",
127 )
128 .label_color(Color::RED, ControlState::Default)
129 .no_tooltip()
130 .build_widget(ctx, "warning2")
131 };
132
133 self.bottom_panel = make_bottom_panel(
134 ctx,
135 app,
136 &self.appwide_panel,
137 Widget::col(vec![
138 format!(
139 "Area: {}",
140 app.partitioning()
141 .neighbourhood_area_km2(self.neighbourhood.id)
142 )
143 .text_widget(ctx)
144 .centered_horiz(),
145 warning1.centered_horiz(),
146 warning2.centered_horiz(),
147 ])
148 .centered_vert(),
149 );
150 }
151}
152
153impl State<App> for DesignLTN {
154 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
155 if let Some(t) = self
156 .appwide_panel
157 .event(ctx, app, &self.preserve_state, help)
158 {
159 return t;
160 }
161 if let Some(t) = app.session.layers.event(
162 ctx,
163 &app.cs,
164 Mode::ModifyNeighbourhood,
165 Some(&self.bottom_panel),
166 ) {
167 return t;
168 }
169 if let Outcome::Clicked(x) = self.bottom_panel.event(ctx) {
170 if x == "Advanced" {
171 return launch_advanced(ctx, app, self.neighbourhood.id);
172 } else if x == "warning1" {
173 return Transition::Push(PopupMsg::new_state(
174 ctx,
175 "Unreachable cell",
176 vec![
177 "Some streets inside this area can't be reached by car at all.",
178 "You probably drew too many filters.",
179 "",
180 "(This may be incorrectly detected near some private/gated roads)",
181 ],
182 ));
183 } else if x == "warning2" {
184 return Transition::Push(PopupMsg::new_state(
185 ctx,
186 "Unusual perimeter",
187 vec![
188 "Part of this area's perimeter consists of streets classified as local.",
189 "This is usually fine, when this area doesn't connect to other main roads farther away.",
190 "If you're near the edge of the map, it might be an error. Try importing a larger area, including the next major road in that direction",
191 ],
192 ));
193 }
194
195 match self.edit.handle_panel_action(
196 ctx,
197 app,
198 x.as_ref(),
199 &self.neighbourhood,
200 &mut self.bottom_panel,
201 ) {
202 EditOutcome::Nothing => unreachable!(),
203 EditOutcome::UpdatePanelAndWorld => {
204 self.update(ctx, app);
205 return Transition::Keep;
206 }
207 EditOutcome::UpdateAll => {
208 if app.session.manage_proposals {
209 self.appwide_panel = AppwidePanel::new(ctx, app, Mode::ModifyNeighbourhood);
210 }
211 self.neighbourhood.edits_changed(&app.per_map.map);
212 self.update(ctx, app);
213 return Transition::Keep;
214 }
215 EditOutcome::Transition(t) => {
216 return t;
217 }
218 }
219 }
220
221 match self.edit.event(ctx, app, &self.neighbourhood) {
222 EditOutcome::Nothing => {}
223 EditOutcome::UpdatePanelAndWorld => {
224 self.update(ctx, app);
225 }
226 EditOutcome::UpdateAll => {
227 if app.session.manage_proposals {
228 self.appwide_panel = AppwidePanel::new(ctx, app, Mode::ModifyNeighbourhood);
229 }
230 self.neighbourhood.edits_changed(&app.per_map.map);
231 self.update(ctx, app);
232 }
233 EditOutcome::Transition(t) => {
234 return t;
235 }
236 }
237
238 self.highlight_cell.event(ctx);
239
240 Transition::Keep
241 }
242
243 fn draw_baselayer(&self) -> DrawBaselayer {
244 DrawBaselayer::Custom
245 }
246
247 fn draw(&self, g: &mut GfxCtx, app: &App) {
248 app.draw_with_layering(g, |g| g.redraw(&self.draw_under_roads_layer));
249 g.redraw(&self.fade_irrelevant);
250 self.draw_top_layer.draw(g);
251 self.highlight_cell.draw(g);
252 self.edit.world.draw(g);
253
254 self.appwide_panel.draw(g);
255 self.bottom_panel.draw(g);
256 self.labels.draw(g);
257 app.per_map.draw_major_road_labels.draw(g);
258 app.session.layers.draw(g, app);
259 app.per_map.draw_all_filters.draw(g);
260 app.per_map.draw_poi_icons.draw(g);
261
262 if self.bottom_panel.currently_hovering() == Some(&"warning1".to_string()) {
263 g.redraw(&self.show_unreachable_cell);
264 }
265 if self.bottom_panel.currently_hovering() == Some(&"warning2".to_string()) {
266 g.redraw(&self.show_suspicious_perimeters);
267 }
268
269 if let EditMode::FreehandFilters(ref lasso) = app.session.edit_mode {
270 lasso.draw(g);
271 }
272 }
273
274 fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
275 Self::new_state(ctx, app, self.neighbourhood.id)
276 }
277}
278
279fn setup_editing(
280 ctx: &mut EventCtx,
281 app: &App,
282 neighbourhood: &Neighbourhood,
283 labels: &DrawSimpleRoadLabels,
284) -> (
285 EditNeighbourhood,
286 Drawable,
287 Drawable,
288 render::RenderCells,
289 World<DummyID>,
290) {
291 let edit = EditNeighbourhood::new(ctx, app, neighbourhood);
292 let map = &app.per_map.map;
293
294 let mut draw_top_layer = GeomBatch::new();
296 let mut highlight_cell = World::new();
299
300 let render_cells = render::RenderCells::new(map, neighbourhood);
301
302 let draw_under_roads_layer = render_cells.draw_colored_areas();
303 draw_top_layer.append(render_cells.draw_island_outlines());
304
305 for (idx, polygons) in render_cells.polygons_per_cell.iter().enumerate() {
307 if polygons.is_empty() {
309 continue;
310 }
311
312 let color = render_cells.colors[idx].alpha(1.0);
313 let mut batch = GeomBatch::new();
314 for arrow in neighbourhood.cells[idx].border_arrows(app) {
315 batch.push(color, arrow);
316 }
317
318 highlight_cell
319 .add_unnamed()
320 .hitboxes(polygons.clone())
321 .drawn_in_master_batch()
323 .draw_hovered(batch)
324 .build(ctx);
325 }
326 highlight_cell.initialize_hover(ctx);
327
328 if !matches!(
329 app.session.edit_mode,
330 EditMode::Shortcuts(_) | EditMode::SpeedLimits
331 ) {
332 draw_top_layer.append(neighbourhood.shortcuts.draw_heatmap(app));
333 }
334
335 for (idx, cell) in neighbourhood.cells.iter().enumerate() {
337 let color = render_cells.colors[idx].alpha(1.0);
338 for arrow in cell.border_arrows(app) {
339 draw_top_layer.push(color, arrow.clone());
340 draw_top_layer.push(Color::BLACK, arrow.to_outline(Distance::meters(1.0)));
341 }
342 }
343
344 let private_road = GeomBatch::load_svg(ctx, "system/assets/map/private_road.svg");
346
347 for r in neighbourhood
348 .interior_roads
349 .iter()
350 .chain(neighbourhood.perimeter_roads.iter())
351 {
352 let road = map.get_r(*r);
353 if let Some(dir) = road.oneway_for_driving() {
354 let thickness = 0.2 * road.get_width();
359 let arrow_len = 5.0 * thickness;
360
361 let slices = if let Some((start, end)) = labels.label_covers_road.get(r) {
362 vec![
363 road.center_pts.exact_slice(Distance::ZERO, *start),
364 road.center_pts.exact_slice(*end, road.length()),
365 ]
366 } else {
367 vec![road.center_pts.clone()]
368 };
369
370 let mut draw_arrow = |pt: Pt2D, angle: Angle| {
371 let pl = PolyLine::must_new(vec![
374 pt.project_away(arrow_len / 2.0, angle.opposite()),
375 pt.project_away(arrow_len / 2.0, angle),
376 ])
377 .maybe_reverse(dir == Direction::Back);
378
379 draw_top_layer.push(
380 colors::LOCAL_ROAD_LABEL,
381 pl.make_arrow(thickness, ArrowCap::Triangle)
382 .to_outline(thickness / 4.0),
383 );
384 };
385
386 let mut any = false;
387 for slice in slices {
388 for (pt, angle) in slice.step_along(3.0 * arrow_len, arrow_len) {
389 any = true;
390 draw_arrow(pt, angle);
391 }
392 }
393
394 if !any {
395 for dist in [Distance::ZERO, road.length()] {
398 let (pt, angle) = road.center_pts.must_dist_along(dist);
399 draw_arrow(pt, angle);
400 }
401 }
402 }
403
404 if is_private(road) {
407 let width = road.get_width() - Distance::meters(2.0);
409 for (dist, rotate) in [(width, 90.0), (road.center_pts.length() - width, -90.0)] {
410 if let Ok((pt, angle)) = road.center_pts.dist_along(dist) {
411 draw_top_layer.append(
412 private_road
413 .clone()
414 .scale_to_fit_width(width.inner_meters())
415 .centered_on(pt)
416 .rotate_around_batch_center(angle.rotate_degs(rotate)),
417 );
418 }
419 }
420 }
421 }
422
423 (
424 edit,
425 draw_top_layer.build(ctx),
426 ctx.upload(draw_under_roads_layer),
427 render_cells,
428 highlight_cell,
429 )
430}
431
432fn launch_advanced(ctx: &mut EventCtx, app: &App, id: NeighbourhoodID) -> Transition {
433 let mut choices = vec![Choice::string("Automatically place modal filters")];
434 if !app.partitioning().custom_boundaries.contains_key(&id) {
435 choices.push(Choice::string("Customize boundary (for drawing only)"));
436 choices.push(Choice::string("Convert to freehand area"));
437 }
438
439 Transition::Push(ChooseSomething::new_state(
440 ctx,
441 "Advanced features",
442 choices,
443 Box::new(move |choice, ctx, app| {
444 if choice == "Customize boundary (for drawing only)" {
445 Transition::Replace(pages::CustomizeBoundary::new_state(ctx, app, id))
446 } else if choice == "Convert to freehand area" {
447 Transition::Replace(pages::FreehandBoundary::new_from_polygon(
448 ctx,
449 app,
450 format!("Converted from {:?}", id),
451 app.partitioning().get_info(id).block.polygon.clone(),
452 ))
453 } else {
454 Transition::Replace(ChooseSomething::new_state(
455 ctx,
456 "Add one filter automatically, using different heuristics",
457 AutoFilterHeuristic::choices(),
458 Box::new(move |heuristic, ctx, app| {
459 match ctx.loading_screen(
460 "automatically filter a neighbourhood",
461 |ctx, timer| {
462 let neighbourhood = Neighbourhood::new(app, id);
463 heuristic.apply(ctx, app, &neighbourhood, timer)
464 },
465 ) {
466 Ok(()) => {
467 Transition::Multi(vec![Transition::Pop, Transition::Recreate])
468 }
469 Err(err) => Transition::Replace(PopupMsg::new_state(
470 ctx,
471 "Error",
472 vec![err.to_string()],
473 )),
474 }
475 }),
476 ))
477 }
478 }),
479 ))
480}
481
482fn help() -> Vec<&'static str> {
483 vec![
484 "The colored cells show where it's possible to drive without leaving the neighbourhood.",
485 "",
486 "The darker red roads have more predicted shortcutting traffic.",
487 "",
488 "Hint: You can place filters at roads or intersections.",
489 "Use the lasso tool to quickly sketch your idea.",
490 ]
491}
492
493fn make_bottom_panel(
494 ctx: &mut EventCtx,
495 app: &App,
496 appwide_panel: &AppwidePanel,
497 per_tab_contents: Widget,
498) -> Panel {
499 let (road_filters, diagonal_filters, one_ways, turn_restrictions) = count_edits(app);
500
501 let row = Widget::row(vec![
502 edit_mode(ctx, app),
503 if let EditMode::Shortcuts(ref focus) = app.session.edit_mode {
504 super::shortcuts::widget(ctx, app, focus.as_ref())
505 } else if let EditMode::SpeedLimits = app.session.edit_mode {
506 super::speed_limits::widget(ctx)
507 } else if let EditMode::TurnRestrictions(ref focus) = app.session.edit_mode {
508 super::turn_restrictions::widget(ctx, app, focus.as_ref())
509 } else {
510 Widget::nothing()
511 }
512 .named("edit mode contents"),
513 Widget::vertical_separator(ctx),
514 Widget::row(vec![
515 ctx.style()
516 .btn_plain
517 .icon("system/assets/tools/undo.svg")
518 .disabled(app.per_map.map.get_edits().commands.is_empty())
520 .hotkey(lctrl(Key::Z))
521 .build_widget(ctx, "undo"),
522 Widget::col(vec![
523 format!("{} new filters", road_filters + diagonal_filters).text_widget(ctx),
524 format!("{} turn restrictions changed", turn_restrictions).text_widget(ctx),
525 format!("{} road directions changed", one_ways).text_widget(ctx),
526 ]),
527 ]),
528 Widget::vertical_separator(ctx),
529 per_tab_contents,
530 if app.per_map.consultation.is_none() {
531 Widget::row(vec![
532 Widget::vertical_separator(ctx),
533 ctx.style()
534 .btn_outline
535 .text("Adjust boundary")
536 .hotkey(Key::B)
537 .build_def(ctx),
538 ctx.style()
539 .btn_outline
540 .text("Per-resident route impact")
541 .build_def(ctx),
542 ctx.style().btn_outline.text("Advanced").build_def(ctx),
543 ])
544 .centered_vert()
545 } else {
546 Widget::row(vec![
547 Widget::vertical_separator(ctx),
548 ctx.style()
549 .btn_outline
550 .text("Per-resident route impact")
551 .build_def(ctx),
552 ])
553 .centered_vert()
554 },
555 ])
556 .evenly_spaced();
557
558 BottomPanel::new(ctx, appwide_panel, row)
559}
560
561fn count_edits(app: &App) -> (usize, usize, usize, usize) {
562 let map = &app.per_map.map;
563 let mut road_filters = 0;
564 let mut diagonal_filters = 0;
565 let mut one_ways = 0;
566 let mut turn_restrictions = 0;
567
568 for (r, orig) in &map.get_edits().original_roads {
569 let road = map.get_r(*r);
570 if road.modal_filter.is_some() && orig.modal_filter.is_none() {
572 road_filters += 1;
573 }
574 let dir_new = road.lanes.iter().map(|l| l.dir).collect::<Vec<_>>();
575 let dir_old = orig.lanes_ltr.iter().map(|l| l.dir).collect::<Vec<_>>();
576 if dir_new != dir_old {
578 one_ways += 1;
579 }
580 let mut tr_added = road.turn_restrictions.clone();
582 tr_added.retain(|x| !orig.turn_restrictions.contains(x));
583 let mut tr_removed = orig.turn_restrictions.clone();
584 tr_removed.retain(|x| !road.turn_restrictions.contains(x));
585 let mut ctr_added = road.complicated_turn_restrictions.clone();
586 ctr_added.retain(|x| !orig.complicated_turn_restrictions.contains(x));
587 let mut ctr_removed = orig.complicated_turn_restrictions.clone();
588 ctr_removed.retain(|x| !road.complicated_turn_restrictions.contains(x));
589 turn_restrictions +=
590 tr_added.len() + tr_removed.len() + ctr_added.len() + ctr_removed.len();
591 }
592 for (i, orig) in &map.get_edits().original_intersections {
593 if map.get_i(*i).modal_filter.is_some() && orig.modal_filter.is_none() {
594 diagonal_filters += 1;
595 }
596 }
597
598 (road_filters, diagonal_filters, one_ways, turn_restrictions)
599}
600
601fn edit_mode(ctx: &mut EventCtx, app: &App) -> Widget {
602 let edit_mode = &app.session.edit_mode;
603 let hide_color = render::filter_hide_color(app.session.filter_type);
604 let name = match app.session.filter_type {
605 FilterType::WalkCycleOnly => "Modal filter -- walking/cycling only",
606 FilterType::NoEntry => "Modal filter - no entry",
607 FilterType::BusGate => "Bus gate",
608 FilterType::SchoolStreet => "School street",
609 };
610
611 Widget::row(vec![
612 Widget::custom_row(vec![
613 ctx.style()
614 .btn_solid_primary
615 .icon(render::filter_svg_path(app.session.filter_type))
616 .image_color(
617 RewriteColor::Change(hide_color, Color::CLEAR),
618 ControlState::Default,
619 )
620 .image_color(
621 RewriteColor::Change(hide_color, Color::CLEAR),
622 ControlState::Disabled,
623 )
624 .disabled(matches!(edit_mode, EditMode::Filters))
625 .tooltip_and_disabled({
626 let mut txt = Text::new();
627 txt.add_line(Line(Key::F1.describe()).fg(ctx.style().text_hotkey_color));
628 txt.append(Line(" - "));
629 txt.append(Line(name));
630 txt.add_line(Line("Click").fg(ctx.style().text_hotkey_color));
631 txt.append(Line(
632 " a road or intersection to add or remove a modal filter",
633 ));
634 txt
635 })
636 .hotkey(Key::F1)
637 .build_widget(ctx, name)
638 .centered_vert(),
639 ctx.style()
640 .btn_plain
641 .dropdown()
642 .build_widget(ctx, "Change modal filter")
643 .centered_vert(),
644 ]),
645 ctx.style()
646 .btn_solid_primary
647 .icon("system/assets/tools/select.svg")
648 .disabled(matches!(edit_mode, EditMode::FreehandFilters(_)))
649 .hotkey(Key::F2)
650 .tooltip_and_disabled({
651 let mut txt = Text::new();
652 txt.add_line(Line(Key::F2.describe()).fg(ctx.style().text_hotkey_color));
653 txt.append(Line(" - Freehand filters"));
654 txt.add_line(Line("Click and drag").fg(ctx.style().text_hotkey_color));
655 txt.append(Line(" across the roads you want to filter"));
656 txt
657 })
658 .build_widget(ctx, "Freehand filters")
659 .centered_vert(),
660 ctx.style()
661 .btn_solid_primary
662 .icon("system/assets/tools/one_ways.svg")
663 .disabled(matches!(edit_mode, EditMode::Oneways))
664 .hotkey(Key::F3)
665 .tooltip_and_disabled({
666 let mut txt = Text::new();
667 txt.add_line(Line(Key::F3.describe()).fg(ctx.style().text_hotkey_color));
668 txt.append(Line(" - One-ways"));
669 txt.add_line(Line("Click").fg(ctx.style().text_hotkey_color));
670 txt.append(Line(" a road to change its direction"));
671 txt
672 })
673 .build_widget(ctx, "One-ways")
674 .centered_vert(),
675 ctx.style()
676 .btn_solid_primary
677 .icon("system/assets/tools/shortcut.svg")
678 .disabled(matches!(edit_mode, EditMode::Shortcuts(_)))
679 .hotkey(Key::F4)
680 .tooltip_and_disabled({
681 let mut txt = Text::new();
682 txt.add_line(Line(Key::F4.describe()).fg(ctx.style().text_hotkey_color));
683 txt.append(Line(" - Shortcuts"));
684 txt.add_line(Line("Click").fg(ctx.style().text_hotkey_color));
685 txt.append(Line(" a road to view shortcuts through it"));
686 txt
687 })
688 .build_widget(ctx, "Shortcuts")
689 .centered_vert(),
690 ctx.style()
691 .btn_solid_primary
692 .icon("system/assets/tools/20_mph.svg")
693 .image_color(
694 RewriteColor::Change(Color::RED, Color::CLEAR),
695 ControlState::Default,
696 )
697 .image_color(
698 RewriteColor::Change(Color::RED, Color::CLEAR),
699 ControlState::Disabled,
700 )
701 .disabled(matches!(edit_mode, EditMode::SpeedLimits))
702 .hotkey(Key::F5)
703 .tooltip_and_disabled({
704 let mut txt = Text::new();
705 txt.add_line(Line(Key::F5.describe()).fg(ctx.style().text_hotkey_color));
706 txt.append(Line(" - Speed limits"));
707 txt.add_line(Line("Click").fg(ctx.style().text_hotkey_color));
708 txt.append(Line(" a road to convert it to 20mph (32kph)"));
709 txt
710 })
711 .build_widget(ctx, "Speed limits")
712 .centered_vert(),
713 ctx.style()
714 .btn_solid_primary
715 .icon("system/assets/map/no_right_turn_button.svg")
716 .disabled(matches!(edit_mode, EditMode::TurnRestrictions(_)))
717 .hotkey(Key::F6)
718 .tooltip_and_disabled({
719 let mut txt = Text::new();
720 txt.add_line(Line(Key::F6.describe()).fg(ctx.style().text_hotkey_color));
721 txt.append(Line(" - Turn restrictions"));
722 txt.add_line(Line("Click").fg(ctx.style().text_hotkey_color));
723 txt.append(Line(
724 " a road to edit turn restrictions at its intersections",
725 ));
726 txt
727 })
728 .build_widget(ctx, "Turn restrictions")
729 .centered_vert(),
730 ])
731}