game/common/
select.rs

1use std::collections::BTreeSet;
2
3use crate::ID;
4use map_gui::tools::intersections_from_roads;
5use map_model::{IntersectionID, RoadID};
6use widgetry::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, Key, RewriteColor, Widget};
7
8use crate::app::App;
9use crate::common::CommonState;
10
11/// A tool for selecting multiple roads.
12pub struct RoadSelector {
13    pub roads: BTreeSet<RoadID>,
14    /// Intersections can't be selected directly. If all roads connected to an intersection are
15    /// selected, then the intersection will be too.
16    pub intersections: BTreeSet<IntersectionID>,
17    pub preview: Option<Drawable>,
18    mode: Mode,
19    dragging: bool,
20}
21
22pub enum Mode {
23    /// No selecting, just normal click-and-drag controls.
24    Pan,
25    /// The user is choosing two intersections, to select the route between.
26    Route {
27        i1: Option<IntersectionID>,
28        preview_path: Option<(IntersectionID, Vec<RoadID>, Drawable)>,
29    },
30    /// Click and drag to select roads
31    Paint,
32    /// Click and drag to unselect roads
33    Erase,
34}
35
36impl RoadSelector {
37    pub fn new(ctx: &mut EventCtx, app: &mut App, roads: BTreeSet<RoadID>) -> RoadSelector {
38        app.primary.current_selection = None;
39        let mut rs = RoadSelector {
40            roads,
41            intersections: BTreeSet::new(),
42            preview: None,
43            mode: Mode::Pan,
44            dragging: false,
45        };
46        rs.roads_changed(ctx, app);
47        rs
48    }
49
50    pub fn make_controls(&self, ctx: &mut EventCtx) -> Widget {
51        Widget::custom_row(vec![
52            ctx.style()
53                .btn_plain
54                .icon("system/assets/tools/pencil.svg")
55                .disabled(matches!(self.mode, Mode::Paint))
56                .hotkey(Key::P)
57                .build_widget(ctx, "paint"),
58            ctx.style()
59                .btn_plain
60                .icon("system/assets/tools/eraser.svg")
61                .hotkey(Key::Backspace)
62                .disabled(matches!(self.mode, Mode::Erase))
63                .build_widget(ctx, "erase"),
64            ctx.style()
65                .btn_plain
66                .icon("system/assets/tools/pin.svg")
67                .hotkey(Key::R)
68                .disabled(matches!(self.mode, Mode::Route { .. }))
69                .build_widget(ctx, "select along route"),
70            ctx.style()
71                .btn_plain
72                .icon("system/assets/tools/pan.svg")
73                .hotkey(Key::Escape)
74                .disabled(matches!(self.mode, Mode::Pan))
75                .build_widget(ctx, "pan"),
76        ])
77        .evenly_spaced()
78    }
79
80    fn roads_changed(&mut self, ctx: &mut EventCtx, app: &App) {
81        let mut batch = GeomBatch::new();
82        for r in &self.roads {
83            batch.push(
84                Color::BLUE.alpha(0.5),
85                app.primary.map.get_r(*r).get_thick_polygon(),
86            );
87        }
88        self.intersections.clear();
89        for i in intersections_from_roads(&self.roads, &app.primary.map) {
90            self.intersections.insert(i);
91            batch.push(
92                Color::BLUE.alpha(0.5),
93                app.primary.map.get_i(i).polygon.clone(),
94            );
95        }
96        self.preview = Some(ctx.upload(batch));
97    }
98
99    // Pass None. Returns true if anything changed.
100    pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App, clicked: Option<&str>) -> bool {
101        if ctx.redo_mouseover() {
102            match self.mode {
103                Mode::Pan => {
104                    app.primary.current_selection = None;
105                }
106                Mode::Route { .. } => {
107                    app.primary.current_selection = app.mouseover_unzoomed_intersections(ctx);
108                }
109                Mode::Paint | Mode::Erase => {
110                    app.primary.current_selection =
111                        match app.mouseover_unzoomed_roads_and_intersections(ctx) {
112                            Some(ID::Road(r)) => Some(r),
113                            Some(ID::Lane(l)) => Some(l.road),
114                            _ => None,
115                        }
116                        .and_then(|r| {
117                            if app.primary.map.get_r(r).is_light_rail() {
118                                None
119                            } else {
120                                Some(ID::Road(r))
121                            }
122                        });
123                }
124            }
125        }
126
127        match self.mode {
128            Mode::Pan | Mode::Route { .. } => {
129                ctx.canvas_movement();
130            }
131            Mode::Paint | Mode::Erase => {
132                if self.dragging && ctx.input.left_mouse_button_released() {
133                    self.dragging = false;
134                } else if !self.dragging && ctx.input.left_mouse_button_pressed() {
135                    self.dragging = true;
136                }
137            }
138        }
139
140        if self.dragging {
141            if let Some(ID::Road(r)) = app.primary.current_selection {
142                let change = match self.mode {
143                    Mode::Paint => {
144                        if self.roads.contains(&r) {
145                            false
146                        } else {
147                            self.roads.insert(r);
148                            true
149                        }
150                    }
151                    Mode::Erase => {
152                        if self.roads.contains(&r) {
153                            self.roads.remove(&r);
154                            true
155                        } else {
156                            false
157                        }
158                    }
159                    Mode::Route { .. } | Mode::Pan => unreachable!(),
160                };
161                if change {
162                    self.roads_changed(ctx, app);
163                    return true;
164                }
165            }
166        }
167
168        if let Some(x) = clicked {
169            match x {
170                "paint" => {
171                    self.dragging = false;
172                    self.mode = Mode::Paint;
173                    return true;
174                }
175                "erase" => {
176                    self.dragging = false;
177                    self.mode = Mode::Erase;
178                    return true;
179                }
180                "pan" => {
181                    app.primary.current_selection = None;
182                    self.dragging = false;
183                    self.mode = Mode::Pan;
184                    return true;
185                }
186                "select along route" => {
187                    app.primary.current_selection = None;
188                    self.dragging = false;
189                    self.mode = Mode::Route {
190                        i1: None,
191                        preview_path: None,
192                    };
193                    return true;
194                }
195                _ => unreachable!(),
196            }
197        }
198
199        if let Mode::Route {
200            ref mut i1,
201            ref mut preview_path,
202        } = self.mode
203        {
204            if let Some(ID::Intersection(i)) = app.primary.current_selection {
205                if i1.is_none() && app.per_obj.left_click(ctx, "start here") {
206                    *i1 = Some(i);
207                }
208            }
209
210            if let Some(i1) = *i1 {
211                if let Some(ID::Intersection(i2)) = app.primary.current_selection {
212                    if preview_path
213                        .as_ref()
214                        .map(|(i, _, _)| *i != i2)
215                        .unwrap_or(true)
216                    {
217                        let mut batch = GeomBatch::new();
218                        let roads = if let Some((roads, intersections)) =
219                            app.primary.map.simple_path_btwn(i1, i2)
220                        {
221                            for r in &roads {
222                                batch.push(
223                                    Color::RED.alpha(0.5),
224                                    app.primary.map.get_r(*r).get_thick_polygon(),
225                                );
226                            }
227                            for i in intersections {
228                                batch.push(
229                                    Color::RED.alpha(0.5),
230                                    app.primary.map.get_i(i).polygon.clone(),
231                                );
232                            }
233                            roads
234                        } else {
235                            Vec::new()
236                        };
237                        *preview_path = Some((i2, roads, ctx.upload(batch)));
238                    }
239
240                    if preview_path
241                        .as_ref()
242                        .map(|(_, roads, _)| !roads.is_empty())
243                        .unwrap_or(false)
244                        && app.per_obj.left_click(ctx, "end here")
245                    {
246                        let (_, roads, _) = preview_path.take().unwrap();
247                        self.roads.extend(roads);
248                        self.mode = Mode::Pan;
249                        self.roads_changed(ctx, app);
250                        return true;
251                    }
252                } else {
253                    *preview_path = None;
254                }
255            }
256        }
257
258        false
259    }
260
261    pub fn draw(&self, g: &mut GfxCtx, app: &App, show_preview: bool) {
262        if show_preview {
263            if let Some(ref p) = self.preview {
264                g.redraw(p);
265            }
266        }
267        if g.canvas.get_cursor_in_map_space().is_some() {
268            if let Some(cursor) = match self.mode {
269                Mode::Pan => None,
270                Mode::Paint => Some("system/assets/tools/pencil.svg"),
271                Mode::Erase => Some("system/assets/tools/eraser.svg"),
272                Mode::Route { .. } => Some("system/assets/tools/pin.svg"),
273            } {
274                let batch = GeomBatch::load_svg(g, cursor)
275                    .centered_on(g.canvas.get_cursor().to_pt())
276                    .color(RewriteColor::ChangeAll(Color::GREEN));
277                g.fork_screenspace();
278                batch.draw(g);
279                g.unfork();
280            }
281        }
282
283        if let Mode::Route {
284            ref i1,
285            ref preview_path,
286        } = self.mode
287        {
288            if let Some(i) = i1 {
289                g.draw_polygon(Color::GREEN, app.primary.map.get_i(*i).polygon.clone());
290            }
291            if let Some((_, _, ref p)) = preview_path {
292                g.redraw(p);
293            }
294        }
295
296        CommonState::draw_osd(g, app);
297    }
298}