turn edit mode's lane brushes into a proper Composite

This commit is contained in:
Dustin Carlino 2020-01-17 13:44:58 -08:00
parent 6bdd557c80
commit 4327322ea4
2 changed files with 135 additions and 157 deletions

View File

@ -2,166 +2,75 @@ use crate::edit::apply_map_edits;
use crate::game::{msg, State, Transition, WizardState}; use crate::game::{msg, State, Transition, WizardState};
use crate::helpers::ID; use crate::helpers::ID;
use crate::ui::UI; use crate::ui::UI;
use ezgui::{hotkey, Button, Choice, Color, EventCtx, GfxCtx, Key, ScreenPt}; use ezgui::{
hotkey, Button, Choice, Color, Composite, EventCtx, GfxCtx, HorizontalAlignment, Key,
ManagedWidget, Outcome, VerticalAlignment,
};
use map_model::{ use map_model::{
connectivity, EditCmd, IntersectionType, LaneID, LaneType, Map, PathConstraints, RoadID, connectivity, EditCmd, IntersectionType, LaneID, LaneType, Map, PathConstraints, RoadID,
}; };
use std::collections::BTreeSet; use std::collections::BTreeSet;
pub struct LaneEditor { pub struct LaneEditor {
brushes: Vec<Paintbrush>, pub brush: Brush,
pub active_idx: Option<usize>, composite: Composite,
pub construction_idx: usize,
reverse_idx: usize,
} }
struct Paintbrush { #[derive(Clone, Copy, PartialEq)]
btn: Button, pub enum Brush {
enabled_btn: Button, Inactive,
label: String, Driving,
// If this returns a string error message, the edit didn't work. If it returns Ok(None), then Bike,
// it's a no-op. Bus,
apply: Box<dyn Fn(&Map, LaneID) -> Result<Option<EditCmd>, String>>, Parking,
Construction,
Reverse,
} }
impl LaneEditor { impl LaneEditor {
pub fn setup(ctx: &EventCtx) -> LaneEditor { pub fn new(ctx: &mut EventCtx) -> LaneEditor {
// TODO This won't handle resizing well
let mut x1 = 0.5 * ctx.canvas.window_width;
let mut make_brush =
|icon: &str,
label: &str,
key: Key,
apply: Box<dyn Fn(&Map, LaneID) -> Result<Option<EditCmd>, String>>| {
let btn = Button::rectangle_svg_bg(
&format!("assets/edit/{}.svg", icon),
label,
hotkey(key),
Color::grey(0.4),
Color::ORANGE,
ctx,
)
.at(ScreenPt::new(x1, 0.0));
let enabled_btn = Button::rectangle_svg_bg(
&format!("assets/edit/{}.svg", icon),
label,
hotkey(key),
Color::RED,
Color::ORANGE,
ctx,
)
.at(ScreenPt::new(x1, 0.0));
x1 += 70.0;
Paintbrush {
btn,
enabled_btn,
label: label.to_string(),
apply,
}
};
let brushes = vec![
make_brush(
"driving",
"driving lane",
Key::D,
Box::new(|map, l| try_change_lane_type(l, LaneType::Driving, map)),
),
make_brush(
"bike",
"protected bike lane",
Key::B,
Box::new(|map, l| try_change_lane_type(l, LaneType::Biking, map)),
),
make_brush(
"bus",
"bus-only lane",
Key::T,
Box::new(|map, l| try_change_lane_type(l, LaneType::Bus, map)),
),
make_brush(
"parking",
"on-street parking lane",
Key::P,
Box::new(|map, l| try_change_lane_type(l, LaneType::Parking, map)),
),
make_brush(
"construction",
"closed for construction",
Key::C,
Box::new(|map, l| try_change_lane_type(l, LaneType::Construction, map)),
),
make_brush(
"contraflow",
"reverse lane direction",
Key::F,
Box::new(|map, l| {
let lane = map.get_l(l);
if !lane.lane_type.is_for_moving_vehicles() {
return Err(format!("You can't reverse a {:?} lane", lane.lane_type));
}
if map.get_r(lane.parent).dir_and_offset(l).1 != 0 {
return Err(format!(
"You can only reverse the lanes next to the road's yellow center line"
));
}
Ok(Some(EditCmd::ReverseLane {
l,
dst_i: lane.src_i,
}))
}),
),
];
let construction_idx = brushes
.iter()
.position(|b| b.label == "closed for construction")
.unwrap();
let reverse_idx = brushes
.iter()
.position(|b| b.label == "reverse lane direction")
.unwrap();
LaneEditor { LaneEditor {
brushes, brush: Brush::Inactive,
active_idx: None, composite: make_brush_panel(ctx, Brush::Inactive),
construction_idx,
reverse_idx,
} }
} }
pub fn event(&mut self, ui: &mut UI, ctx: &mut EventCtx) -> Option<Transition> { pub fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
// TODO This is some awkward way to express mutual exclusion. :( // Change brush
let mut undo_old = None; match self.composite.event(ctx) {
for (idx, p) in self.brushes.iter_mut().enumerate() { Some(Outcome::Clicked(x)) => {
if Some(idx) == undo_old { let b = match x.as_ref() {
p.btn.just_replaced(ctx); "driving lane" => Brush::Driving,
undo_old = None; "protected bike lane" => Brush::Bike,
} "bus-only lane" => Brush::Bus,
"on-street parking lane" => Brush::Parking,
if self.active_idx == Some(idx) { "closed for construction" => Brush::Construction,
p.enabled_btn.event(ctx); "reverse lane direction" => Brush::Reverse,
if p.enabled_btn.clicked() { _ => unreachable!(),
self.active_idx = None; };
p.btn.just_replaced(ctx); if self.brush == b {
} self.brush = Brush::Inactive;
} else { } else {
p.btn.event(ctx); self.brush = b;
if p.btn.clicked() {
undo_old = self.active_idx;
self.active_idx = Some(idx);
p.enabled_btn.just_replaced(ctx);
} }
self.composite = make_brush_panel(ctx, self.brush);
} }
} None => {}
// Have to do this outside the loop where brushes are all mutably borrowed
if let Some(idx) = undo_old {
self.brushes[idx].btn.just_replaced(ctx);
} }
if let Some(ID::Lane(l)) = ui.primary.current_selection { if let Some(ID::Lane(l)) = ui.primary.current_selection {
if let Some(idx) = self.active_idx { // TODO Refactor all of these mappings!
if ui.per_obj.action(ctx, Key::Space, &self.brushes[idx].label) { if self.brush != Brush::Inactive {
let label = match self.brush {
Brush::Inactive => unreachable!(),
Brush::Driving => "driving lane",
Brush::Bike => "protected bike lane",
Brush::Bus => "bus-only lane",
Brush::Parking => "on-street parking lane",
Brush::Construction => "closed for construction",
Brush::Reverse => "reverse lane direction",
};
if ui.per_obj.action(ctx, Key::Space, label) {
// These errors are universal. // These errors are universal.
if ui.primary.map.get_l(l).is_sidewalk() { if ui.primary.map.get_l(l).is_sidewalk() {
return Some(Transition::Push(msg( return Some(Transition::Push(msg(
@ -176,7 +85,7 @@ impl LaneEditor {
))); )));
} }
match (self.brushes[idx].apply)(&ui.primary.map, l) { match apply_brush(self.brush, &ui.primary.map, l) {
Ok(Some(cmd)) => { Ok(Some(cmd)) => {
let mut edits = ui.primary.map.get_edits().clone(); let mut edits = ui.primary.map.get_edits().clone();
edits.commands.push(cmd); edits.commands.push(cmd);
@ -213,7 +122,7 @@ impl LaneEditor {
} }
} else if ui.primary.map.get_edits().reversed_lanes.contains(&l) { } else if ui.primary.map.get_edits().reversed_lanes.contains(&l) {
if ui.per_obj.action(ctx, Key::R, "revert") { if ui.per_obj.action(ctx, Key::R, "revert") {
match (self.brushes[self.reverse_idx].apply)(&ui.primary.map, l) { match apply_brush(Brush::Reverse, &ui.primary.map, l) {
Ok(Some(cmd)) => { Ok(Some(cmd)) => {
let mut edits = ui.primary.map.get_edits().clone(); let mut edits = ui.primary.map.get_edits().clone();
edits.commands.push(cmd); edits.commands.push(cmd);
@ -227,12 +136,13 @@ impl LaneEditor {
} }
} }
} }
// Woo, a special case! The construction tool also applies to intersections. // Woo, a special case! The construction tool also applies to intersections.
if let Some(ID::Intersection(i)) = ui.primary.current_selection { if let Some(ID::Intersection(i)) = ui.primary.current_selection {
if self.active_idx == Some(self.construction_idx) if self.brush == Brush::Construction
&& ui && ui
.per_obj .per_obj
.action(ctx, Key::Space, &self.brushes[self.construction_idx].label) .action(ctx, Key::Space, "closed for construction")
{ {
let it = ui.primary.map.get_i(i).intersection_type; let it = ui.primary.map.get_i(i).intersection_type;
if it != IntersectionType::Construction && it != IntersectionType::Border { if it != IntersectionType::Construction && it != IntersectionType::Border {
@ -267,16 +177,55 @@ impl LaneEditor {
} }
pub fn draw(&self, g: &mut GfxCtx) { pub fn draw(&self, g: &mut GfxCtx) {
for (idx, p) in self.brushes.iter().enumerate() { self.composite.draw(g);
if self.active_idx == Some(idx) {
p.enabled_btn.draw(g);
} else {
p.btn.draw(g);
}
}
} }
} }
fn make_brush_panel(ctx: &mut EventCtx, brush: Brush) -> Composite {
let mut row = Vec::new();
for (b, icon, label, key) in vec![
(Brush::Driving, "driving", "driving lane", Key::D),
(Brush::Bike, "bike", "protected bike lane", Key::B),
(Brush::Bus, "bus", "bus-only lane", Key::T),
(Brush::Parking, "parking", "on-street parking lane", Key::P),
(
Brush::Construction,
"construction",
"closed for construction",
Key::C,
),
(
Brush::Reverse,
"contraflow",
"reverse lane direction",
Key::F,
),
] {
row.push(
ManagedWidget::col(vec![ManagedWidget::btn(Button::rectangle_svg_bg(
&format!("assets/edit/{}.svg", icon),
label,
hotkey(key),
if brush == b {
Color::RED
} else {
Color::grey(0.4)
},
Color::ORANGE,
ctx,
))])
.padding(5),
);
}
Composite::new(
ManagedWidget::row(row)
.bg(Color::hex("#4C4C4C"))
.padding(10),
)
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx)
}
fn can_change_lane_type(l: LaneID, new_lt: LaneType, map: &Map) -> Option<String> { fn can_change_lane_type(l: LaneID, new_lt: LaneType, map: &Map) -> Option<String> {
let r = map.get_parent(l); let r = map.get_parent(l);
let (fwds, idx) = r.dir_and_offset(l); let (fwds, idx) = r.dir_and_offset(l);
@ -405,3 +354,31 @@ fn make_bulk_edit_lanes(road: RoadID) -> Box<dyn State> {
))) )))
})) }))
} }
// If this returns a string error message, the edit didn't work. If it returns Ok(None), then
// it's a no-op.
fn apply_brush(brush: Brush, map: &Map, l: LaneID) -> Result<Option<EditCmd>, String> {
match brush {
Brush::Inactive => unreachable!(),
Brush::Driving => try_change_lane_type(l, LaneType::Driving, map),
Brush::Bike => try_change_lane_type(l, LaneType::Biking, map),
Brush::Bus => try_change_lane_type(l, LaneType::Bus, map),
Brush::Parking => try_change_lane_type(l, LaneType::Parking, map),
Brush::Construction => try_change_lane_type(l, LaneType::Construction, map),
Brush::Reverse => {
let lane = map.get_l(l);
if !lane.lane_type.is_for_moving_vehicles() {
return Err(format!("You can't reverse a {:?} lane", lane.lane_type));
}
if map.get_r(lane.parent).dir_and_offset(l).1 != 0 {
return Err(format!(
"You can only reverse the lanes next to the road's yellow center line"
));
}
Ok(Some(EditCmd::ReverseLane {
l,
dst_i: lane.src_i,
}))
}
}
}

View File

@ -2,6 +2,7 @@ mod lanes;
mod stop_signs; mod stop_signs;
mod traffic_signals; mod traffic_signals;
use self::lanes::{Brush, LaneEditor};
pub use self::traffic_signals::TrafficSignalEditor; pub use self::traffic_signals::TrafficSignalEditor;
use crate::common::{tool_panel, CommonState, Warping}; use crate::common::{tool_panel, CommonState, Warping};
use crate::debug::DebugMode; use crate::debug::DebugMode;
@ -31,7 +32,7 @@ pub struct EditMode {
mode: GameplayMode, mode: GameplayMode,
pub suspended_sim: Sim, pub suspended_sim: Sim,
lane_editor: lanes::LaneEditor, lane_editor: LaneEditor,
} }
impl EditMode { impl EditMode {
@ -53,7 +54,7 @@ impl EditMode {
), ),
mode, mode,
suspended_sim, suspended_sim,
lane_editor: lanes::LaneEditor::setup(ctx), lane_editor: LaneEditor::new(ctx),
} }
} }
} }
@ -85,7 +86,7 @@ impl State for EditMode {
self.menu.event(ctx); self.menu.event(ctx);
if self.mode.can_edit_lanes() { if self.mode.can_edit_lanes() {
if let Some(t) = self.lane_editor.event(ui, ctx) { if let Some(t) = self.lane_editor.event(ctx, ui) {
return t; return t;
} }
} }
@ -95,13 +96,13 @@ impl State for EditMode {
ui.recalculate_current_selection(ctx); ui.recalculate_current_selection(ctx);
if let Some(ID::Lane(_)) = ui.primary.current_selection { if let Some(ID::Lane(_)) = ui.primary.current_selection {
} else if let Some(ID::Intersection(_)) = ui.primary.current_selection { } else if let Some(ID::Intersection(_)) = ui.primary.current_selection {
if self.lane_editor.active_idx != Some(self.lane_editor.construction_idx) if self.lane_editor.brush != Brush::Construction
&& self.lane_editor.active_idx.is_some() && self.lane_editor.brush != Brush::Inactive
{ {
ui.primary.current_selection = None; ui.primary.current_selection = None;
} }
} else { } else {
if self.lane_editor.active_idx.is_some() { if self.lane_editor.brush != Brush::Inactive {
ui.primary.current_selection = None; ui.primary.current_selection = None;
} }
} }