map_gui/
options.rs

1use serde::{Deserialize, Serialize};
2
3use abstutil::Timer;
4use geom::{Duration, UnitFmt};
5use widgetry::{
6    CanvasSettings, Choice, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel, Spinner, State,
7    TextExt, Toggle, Widget,
8};
9
10use crate::colors::ColorSchemeChoice;
11use crate::render::DrawBuilding;
12use crate::tools::grey_out_map;
13use crate::AppLike;
14
15/// Options controlling the UI. Some of the options are common to all map-based apps, and some are
16/// specific to A/B Street.
17// TODO SimOptions stuff too
18#[derive(Clone, Serialize, Deserialize)]
19pub struct Options {
20    /// Dev mode exposes experimental tools useful for debugging, but that'd likely confuse most
21    /// players.
22    pub dev: bool,
23    /// Every time we draw, render all agents zoomed in. Extremely slow. Just used to flush out
24    /// drawing bugs.
25    pub debug_all_agents: bool,
26
27    /// How traffic signals should be rendered.
28    pub traffic_signal_style: TrafficSignalStyle,
29    /// The color scheme for map elements, agents, and the UI.
30    pub color_scheme: ColorSchemeChoice,
31    /// Automatically change color_scheme based on simulation time to reflect day/night
32    pub toggle_day_night_colors: bool,
33    /// Draw buildings in different perspectives
34    pub camera_angle: CameraAngle,
35    /// Draw building driveways.
36    pub show_building_driveways: bool,
37    /// Draw building outlines.
38    pub show_building_outlines: bool,
39    /// Draw stop signs.
40    pub show_stop_signs: bool,
41    /// Draw crosswalks and unmarked crossings.
42    pub show_crosswalks: bool,
43    /// If true, draw an icon for traffic signals both when zoomed and unzoomed. If false, color
44    /// the intersection when unzoomed and render the signal's current state when zoomed.
45    pub show_traffic_signal_icon: bool,
46    /// If true, modify several basemap features to de-emphasize them: border intersections
47    pub simplify_basemap: bool,
48
49    /// When making a screen recording, enable this option to hide some UI elements
50    pub minimal_controls: bool,
51    /// widgetry options
52    pub canvas_settings: CanvasSettings,
53
54    /// How much to advance the sim with one of the speed controls
55    pub time_increment: Duration,
56    /// When time warping, don't draw to speed up simulation
57    pub dont_draw_time_warp: bool,
58    /// The delay threshold to halt on when jumping to the next delay
59    pub jump_to_delay: Duration,
60
61    /// Display roads and buildings in an alternate language, if possible. None means to use the
62    /// OSM native name.
63    pub language: Option<String>,
64    /// How to render geometric units
65    pub units: UnitFmt,
66}
67
68impl Options {
69    /// Restore previous options. If the file is missing or the format has changed, fall back to
70    /// built-in defaults.
71    pub fn load_or_default() -> Options {
72        match abstio::maybe_read_json::<Options>(
73            abstio::path_player("settings.json"),
74            &mut Timer::throwaway(),
75        ) {
76            Ok(opts) => {
77                return opts;
78            }
79            Err(err) => {
80                warn!("Couldn't restore settings, so using defaults. {}", err);
81            }
82        }
83
84        Options {
85            dev: false,
86            debug_all_agents: false,
87
88            traffic_signal_style: TrafficSignalStyle::Brian,
89            color_scheme: ColorSchemeChoice::DayMode,
90            toggle_day_night_colors: false,
91            camera_angle: CameraAngle::TopDown,
92            show_building_driveways: true,
93            show_building_outlines: true,
94            show_stop_signs: true,
95            show_crosswalks: true,
96            show_traffic_signal_icon: false,
97            simplify_basemap: false,
98
99            time_increment: Duration::minutes(10),
100            dont_draw_time_warp: false,
101            jump_to_delay: Duration::minutes(5),
102
103            minimal_controls: false,
104            canvas_settings: CanvasSettings::new(),
105            language: None,
106            units: UnitFmt {
107                round_durations: true,
108                // TODO Should default be based on the map?
109                metric: false,
110            },
111        }
112    }
113}
114
115/// Different ways of drawing traffic signals. The names of these aren't super meaningful...
116#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
117pub enum TrafficSignalStyle {
118    Brian,
119    Yuwen,
120    IndividualTurnArrows,
121}
122
123#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
124pub enum CameraAngle {
125    TopDown,
126    IsometricNE,
127    IsometricNW,
128    IsometricSE,
129    IsometricSW,
130    Abstract,
131}
132
133pub struct OptionsPanel {
134    panel: Panel,
135}
136
137impl OptionsPanel {
138    pub fn new_state<A: AppLike>(ctx: &mut EventCtx, app: &A) -> Box<dyn State<A>> {
139        Box::new(OptionsPanel {
140            panel: Panel::new_builder(Widget::col(vec![
141                Widget::custom_row(vec![
142                    Line("Settings").small_heading().into_widget(ctx),
143                    ctx.style().btn_close_widget(ctx),
144                ]),
145                "Camera controls".text_widget(ctx),
146                Widget::col(vec![
147                    Toggle::checkbox(
148                        ctx,
149                        "Invert direction of vertical scrolling",
150                        None,
151                        ctx.canvas.settings.invert_scroll,
152                    ),
153                    Toggle::checkbox(
154                        ctx,
155                        "Pan map when cursor is at edge of screen",
156                        None,
157                        ctx.canvas.settings.edge_auto_panning,
158                    )
159                    .named("autopan"),
160                    Toggle::checkbox(
161                        ctx,
162                        "Use touchpad to pan and hold Control to zoom",
163                        None,
164                        ctx.canvas.settings.touchpad_to_move,
165                    ),
166                    Toggle::checkbox(
167                        ctx,
168                        "Use arrow keys to pan and Q/W to zoom",
169                        None,
170                        ctx.canvas.settings.keys_to_pan,
171                    ),
172                    Widget::row(vec![
173                        "Scroll speed for menus".text_widget(ctx).centered_vert(),
174                        Spinner::widget(
175                            ctx,
176                            "gui_scroll_speed",
177                            (1, 50),
178                            ctx.canvas.settings.gui_scroll_speed,
179                            1,
180                        ),
181                    ]),
182                    Widget::row(vec![
183                        "Zoom speed for the map".text_widget(ctx).centered_vert(),
184                        Spinner::widget(
185                            ctx,
186                            "canvas_scroll_speed",
187                            (1, 30),
188                            ctx.canvas.settings.canvas_scroll_speed,
189                            1,
190                        ),
191                    ]),
192                ])
193                .bg(app.cs().inner_panel_bg)
194                .padding(8),
195                "Appearance".text_widget(ctx),
196                Widget::col(vec![
197                    Widget::row(vec![
198                        "Traffic signal rendering:".text_widget(ctx),
199                        Widget::dropdown(
200                            ctx,
201                            "Traffic signal rendering",
202                            app.opts().traffic_signal_style.clone(),
203                            vec![
204                                Choice::new("Default (Brian's style)", TrafficSignalStyle::Brian),
205                                Choice::new("Yuwen's style", TrafficSignalStyle::Yuwen),
206                                Choice::new(
207                                    "arrows showing individual turns (to debug)",
208                                    TrafficSignalStyle::IndividualTurnArrows,
209                                ),
210                            ],
211                        ),
212                    ]),
213                    Widget::row(vec![
214                        "Camera angle:".text_widget(ctx),
215                        Widget::dropdown(
216                            ctx,
217                            "Camera angle",
218                            app.opts().camera_angle.clone(),
219                            vec![
220                                Choice::new("Top-down", CameraAngle::TopDown),
221                                Choice::new("Isometric (northeast)", CameraAngle::IsometricNE),
222                                Choice::new("Isometric (northwest)", CameraAngle::IsometricNW),
223                                Choice::new("Isometric (southeast)", CameraAngle::IsometricSE),
224                                Choice::new("Isometric (southwest)", CameraAngle::IsometricSW),
225                                Choice::new("Abstract (just symbols)", CameraAngle::Abstract),
226                            ],
227                        ),
228                    ]),
229                    Widget::row(vec![
230                        "Color scheme:".text_widget(ctx),
231                        Widget::dropdown(
232                            ctx,
233                            "Color scheme",
234                            app.opts().color_scheme,
235                            ColorSchemeChoice::choices(),
236                        ),
237                    ]),
238                    Widget::row(vec![
239                        "Camera zoom to switch to unzoomed view".text_widget(ctx),
240                        Widget::dropdown(
241                            ctx,
242                            "min zoom",
243                            ctx.canvas.settings.min_zoom_for_detail,
244                            vec![
245                                Choice::new("1.0", 1.0),
246                                Choice::new("2.0", 2.0),
247                                Choice::new("3.0", 3.0),
248                                Choice::new("4.0", 4.0),
249                                Choice::new("5.0", 5.0),
250                                Choice::new("6.0", 6.0),
251                            ],
252                        ),
253                    ]),
254                    Widget::row(vec!["Language".text_widget(ctx), {
255                        let mut default = app.opts().language.clone();
256                        let mut have_default = false;
257                        let mut choices = vec![Choice::new("Map native language", None)];
258                        for lang in app.map().get_languages() {
259                            if default.as_ref() == Some(&lang) {
260                                have_default = true;
261                            }
262                            choices.push(Choice::new(lang.clone(), Some(lang)));
263                        }
264                        // We might be switching from a map that has more languages than this
265                        // map
266                        if !have_default {
267                            default = None;
268                        }
269                        Widget::dropdown(ctx, "language", default, choices)
270                    }]),
271                    Toggle::choice(
272                        ctx,
273                        "metric / imperial units",
274                        "metric",
275                        "imperial",
276                        None,
277                        app.opts().units.metric,
278                    ),
279                ])
280                .bg(app.cs().inner_panel_bg)
281                .padding(8),
282                "Debug".text_widget(ctx),
283                Widget::col(vec![
284                    Toggle::checkbox(ctx, "Enable developer mode", None, app.opts().dev),
285                    Toggle::checkbox(
286                        ctx,
287                        "Draw all agents to debug geometry (Slow!)",
288                        None,
289                        app.opts().debug_all_agents,
290                    ),
291                ])
292                .bg(app.cs().inner_panel_bg)
293                .padding(8),
294                ctx.style()
295                    .btn_solid_primary
296                    .text("Apply")
297                    .hotkey(Key::Enter)
298                    .build_def(ctx)
299                    .centered_horiz(),
300            ]))
301            .build(ctx),
302        })
303    }
304}
305
306impl<A: AppLike> State<A> for OptionsPanel {
307    fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> widgetry::Transition<A> {
308        if let Outcome::Clicked(x) = self.panel.event(ctx) {
309            match x.as_ref() {
310                "close" => {
311                    return widgetry::Transition::Pop;
312                }
313                "Apply" => {
314                    let mut opts = app.opts().clone();
315                    opts.dev = self.panel.is_checked("Enable developer mode");
316                    opts.debug_all_agents = self
317                        .panel
318                        .is_checked("Draw all agents to debug geometry (Slow!)");
319
320                    ctx.canvas.settings.invert_scroll = self
321                        .panel
322                        .is_checked("Invert direction of vertical scrolling");
323                    ctx.canvas.settings.touchpad_to_move = self
324                        .panel
325                        .is_checked("Use touchpad to pan and hold Control to zoom");
326                    ctx.canvas.settings.keys_to_pan = self
327                        .panel
328                        .is_checked("Use arrow keys to pan and Q/W to zoom");
329                    ctx.canvas.settings.edge_auto_panning = self.panel.is_checked("autopan");
330                    ctx.canvas.settings.gui_scroll_speed = self.panel.spinner("gui_scroll_speed");
331                    ctx.canvas.settings.canvas_scroll_speed =
332                        self.panel.spinner("canvas_scroll_speed");
333                    ctx.canvas.settings.min_zoom_for_detail = self.panel.dropdown_value("min zoom");
334                    // Copy the settings into the Options struct, so they're saved.
335                    opts.canvas_settings = ctx.canvas.settings.clone();
336
337                    let style = self.panel.dropdown_value("Traffic signal rendering");
338                    if opts.traffic_signal_style != style {
339                        opts.traffic_signal_style = style;
340                        println!("Rerendering traffic signals...");
341                        for i in &mut app.mut_draw_map().intersections {
342                            *i.draw_traffic_signal.borrow_mut() = None;
343                        }
344                    }
345
346                    let camera_angle = self.panel.dropdown_value("Camera angle");
347                    if opts.camera_angle != camera_angle {
348                        opts.camera_angle = camera_angle;
349                        ctx.loading_screen("rerendering buildings", |ctx, timer| {
350                            let mut all_buildings = GeomBatch::new();
351                            let mut all_building_outlines = GeomBatch::new();
352                            timer
353                                .start_iter("rendering buildings", app.map().all_buildings().len());
354                            for b in app.map().all_buildings() {
355                                timer.next();
356                                DrawBuilding::new(
357                                    ctx,
358                                    b,
359                                    app.map(),
360                                    app.cs(),
361                                    &opts,
362                                    &mut all_buildings,
363                                    &mut all_building_outlines,
364                                );
365                            }
366                            for r in &mut app.mut_draw_map().roads {
367                                r.clear_rendering();
368                            }
369
370                            timer.start("upload geometry");
371                            app.mut_draw_map().draw_all_buildings = all_buildings.upload(ctx);
372                            app.mut_draw_map().draw_all_building_outlines =
373                                all_building_outlines.upload(ctx);
374                            timer.stop("upload geometry");
375                        });
376                    }
377
378                    if app.change_color_scheme(ctx, self.panel.dropdown_value("Color scheme")) {
379                        // change_color_scheme doesn't modify our local copy of Options!
380                        opts.color_scheme = app.opts().color_scheme;
381                        // If the player picks a different scheme, don't undo it later.
382                        opts.toggle_day_night_colors = false;
383                    }
384
385                    opts.units.metric = self.panel.is_checked("metric / imperial units");
386
387                    let language = self.panel.dropdown_value("language");
388                    if language != opts.language {
389                        opts.language = language;
390                        for r in &mut app.mut_draw_map().roads {
391                            r.clear_rendering();
392                        }
393                    }
394
395                    // Be careful -- there are some options not exposed by this panel, but per app.
396                    let show_building_driveways = opts.show_building_driveways;
397                    opts.show_building_driveways = true;
398                    let show_building_outlines = opts.show_building_outlines;
399                    opts.show_building_outlines = true;
400                    abstio::write_json(abstio::path_player("settings.json"), &opts);
401                    opts.show_building_driveways = show_building_driveways;
402                    opts.show_building_outlines = show_building_outlines;
403                    *app.mut_opts() = opts;
404
405                    return widgetry::Transition::Pop;
406                }
407                _ => unreachable!(),
408            }
409        }
410
411        widgetry::Transition::Keep
412    }
413
414    fn draw(&self, g: &mut GfxCtx, app: &A) {
415        grey_out_map(g, app);
416        self.panel.draw(g);
417    }
418}