map_gui/tools/
title_screen.rs

1use widgetry::tools::{open_browser, PopupMsg, URLManager};
2use widgetry::{
3    EventCtx, Image, Key, Line, Panel, RewriteColor, SimpleState, State, Transition, Widget,
4};
5
6use crate::AppLike;
7
8/// A title screen shared among all of the A/B Street apps.
9pub struct TitleScreen<A: AppLike + 'static> {
10    current_exe: Executable,
11    enter_state: Box<dyn Fn(&mut EventCtx, &mut A, Vec<&str>) -> Box<dyn State<A>>>,
12}
13
14#[derive(Clone, Copy, PartialEq)]
15pub enum Executable {
16    ABStreet,
17    FifteenMin,
18    OSMViewer,
19    ParkingMapper,
20    Santa,
21    RawMapEditor,
22    LTN,
23}
24
25impl<A: AppLike + 'static> TitleScreen<A> {
26    pub fn new_state(
27        ctx: &mut EventCtx,
28        app: &A,
29        current_exe: Executable,
30        enter_state: Box<dyn Fn(&mut EventCtx, &mut A, Vec<&str>) -> Box<dyn State<A>>>,
31    ) -> Box<dyn State<A>> {
32        let panel = Panel::new_builder(Widget::col(vec![
33            Image::from_path("system/assets/pregame/logo.svg")
34                .untinted()
35                .dims(150.0)
36                .into_widget(ctx),
37            Widget::row(vec![
38                Widget::col(vec![
39                    Line("Games").small_heading().into_widget(ctx),
40                    Widget::row(vec![
41                        Image::from_path("system/assets/pregame/tutorial.svg")
42                            .untinted()
43                            .dims(100.0)
44                            .into_widget(ctx),
45                        ctx.style()
46                            .btn_outline
47                            .text("Traffic simulation tutorial")
48                            .hotkey(Key::T)
49                            .disabled(true)
50                            .disabled_tooltip("Tutorial mode currently unmaintained, sorry")
51                            .build_def(ctx)
52                            .centered_vert(),
53                    ]),
54                    Widget::row(vec![
55                        Image::from_path("system/assets/pregame/challenges.svg")
56                            .untinted()
57                            .dims(100.0)
58                            .into_widget(ctx),
59                        ctx.style()
60                            .btn_outline
61                            .text("Traffic simulation challenges")
62                            .tooltip("Complete specific objectives in the traffic simulator")
63                            .build_def(ctx)
64                            .centered_vert(),
65                    ]),
66                    Widget::row(vec![
67                        Image::from_path("system/assets/santa/bike1.svg")
68                            .untinted()
69                            .dims(100.0)
70                            .into_widget(ctx),
71                        ctx.style()
72                            .btn_outline
73                            .text("15-minute Santa")
74                            .tooltip("Deliver presents as efficiently as possible")
75                            .build_def(ctx)
76                            .centered_vert(),
77                    ]),
78                ])
79                .section(ctx),
80                Widget::col(vec![
81                    Line("Planning").small_heading().into_widget(ctx),
82                    Widget::row(vec![
83                        Image::from_path("system/assets/pregame/sandbox.svg")
84                            .untinted()
85                            .dims(100.0)
86                            .into_widget(ctx),
87                        ctx.style()
88                            .btn_outline
89                            .text("Traffic simulation sandbox")
90                            .hotkey(Key::S)
91                            .tooltip("Simulate traffic, edit streets, measure effects")
92                            .build_def(ctx)
93                            .centered_vert(),
94                    ]),
95                    Widget::row(vec![
96                        Image::from_path("system/assets/edit/bike.svg")
97                            .color(RewriteColor::ChangeAll(app.cs().bike_trip))
98                            .dims(100.0)
99                            .into_widget(ctx),
100                        ctx.style()
101                            .btn_outline
102                            .text("Ungap the Map")
103                            .tooltip("Improve a city's bike network")
104                            .build_def(ctx)
105                            .centered_vert(),
106                    ]),
107                    ctx.style()
108                        .btn_outline
109                        .text("15-minute neighborhoods")
110                        .tooltip("Explore what places residents can easily reach")
111                        .build_def(ctx),
112                    ctx.style()
113                        .btn_outline
114                        .text("Low traffic neighborhoods")
115                        .tooltip("Reduce vehicle shortcuts through residential streets")
116                        .build_def(ctx),
117                    ctx.style()
118                        .btn_outline
119                        .text("ActDev")
120                        .tooltip("Explore mobility patterns around new residential development")
121                        .build_def(ctx),
122                ])
123                .section(ctx),
124                Widget::col(vec![
125                    Line("Other").small_heading().into_widget(ctx),
126                    ctx.style()
127                        .btn_outline
128                        .text("Community proposals")
129                        .tooltip("Try out proposals for changing different cities")
130                        .build_def(ctx),
131                    ctx.style()
132                        .btn_outline
133                        .text("Advanced tools")
134                        .build_def(ctx),
135                    ctx.style().btn_outline.text("About").build_def(ctx),
136                ])
137                .section(ctx),
138            ]),
139            Widget::col(vec![
140                ctx.style()
141                    .btn_outline
142                    .text("Created by Dustin Carlino, Yuwen Li, & Michael Kirk")
143                    .build_widget(ctx, "Credits"),
144                built_info::maybe_update(ctx),
145            ])
146            .centered_horiz()
147            .align_bottom(),
148        ]))
149        .build(ctx);
150        <dyn SimpleState<_>>::new_state(
151            panel,
152            Box::new(TitleScreen {
153                current_exe,
154                enter_state,
155            }),
156        )
157    }
158
159    fn run(
160        &self,
161        ctx: &mut EventCtx,
162        app: &mut A,
163        exe: Executable,
164        args: Vec<&str>,
165    ) -> Transition<A> {
166        if exe == self.current_exe {
167            Transition::Push((self.enter_state)(ctx, app, args))
168        } else {
169            exe.replace_process(ctx, app, args);
170            // On most platforms, this is unreachable. But on Windows, just keep the current app
171            // open.
172            Transition::Keep
173        }
174    }
175}
176
177impl Executable {
178    /// Run the given executable with some arguments. On Mac and Linux, this replaces the current
179    /// process. On Windows, this launches a new child process and leaves the current alone. On
180    /// web, this makes the browser go to a new page.
181    pub fn replace_process<A: AppLike + 'static>(
182        self,
183        ctx: &mut EventCtx,
184        app: &A,
185        args: Vec<&str>,
186    ) -> Transition<A> {
187        let mut args: Vec<String> = args.into_iter().map(|a| a.to_string()).collect();
188        // Usually pass in the current map's path
189        match self {
190            Executable::Santa => {}
191            Executable::RawMapEditor => {
192                args.push(abstio::path_raw_map(app.map().get_name()));
193                args.push(format!(
194                    "--cam={}",
195                    URLManager::get_cam_param(ctx, app.map().get_gps_bounds())
196                ));
197            }
198            _ => {
199                args.push(app.map().get_name().path());
200            }
201        }
202
203        // On native, end the current process and start another.
204        #[cfg(not(target_arch = "wasm32"))]
205        {
206            use std::process::Command;
207
208            // TODO find_exe panics; should return error instead
209            let binary = crate::tools::find_exe(match self {
210                Executable::ABStreet => "game",
211                Executable::FifteenMin => "fifteen_min",
212                Executable::OSMViewer => "osm_viewer",
213                Executable::ParkingMapper => "parking_mapper",
214                Executable::Santa => "santa",
215                Executable::RawMapEditor => "map_editor",
216                Executable::LTN => "ltn",
217            });
218
219            // We can only replace the current process on Linux/Mac
220            #[cfg(not(windows))]
221            {
222                use std::os::unix::process::CommandExt;
223                let err = Command::new(binary).args(args).exec();
224                // We only get here if something broke
225                Transition::Push(PopupMsg::new_state(ctx, "Error", vec![err.to_string()]))
226            }
227
228            // On Windows, all we can do is open a new child process. Not sure how to end the
229            // current or detach.
230            #[cfg(windows)]
231            {
232                abstutil::must_run_cmd(Command::new(binary).args(args));
233                Transition::Keep
234            }
235        }
236
237        // On web, leave the current page and go to another.
238        #[cfg(target_arch = "wasm32")]
239        {
240            fn set_href(url: &str) -> anyhow::Result<()> {
241                let window = web_sys::window().ok_or(anyhow!("no window?"))?;
242                window.location().set_href(url).map_err(|err| {
243                    anyhow!(err
244                        .as_string()
245                        .unwrap_or("window.location.set_href failed".to_string()))
246                })
247            }
248
249            let page = match self {
250                Executable::ABStreet => "abstreet",
251                Executable::FifteenMin => "fifteen_min",
252                Executable::OSMViewer => "osm_viewer",
253                // This only works on native
254                Executable::ParkingMapper => unreachable!(),
255                Executable::Santa => "santa",
256                Executable::RawMapEditor => "map_editor",
257                Executable::LTN => "ltn",
258            };
259            let url = format!("{}.html{}", page, abstutil::args_to_query_string(args));
260            if let Err(err) = set_href(&url) {
261                return Transition::Push(PopupMsg::new_state(
262                    ctx,
263                    "Error",
264                    vec![format!("Couldn't redirect to {}: {}", url, err)],
265                ));
266            }
267            Transition::Keep
268        }
269    }
270}
271
272impl<A: AppLike + 'static> SimpleState<A> for TitleScreen<A> {
273    fn on_click(
274        &mut self,
275        ctx: &mut EventCtx,
276        app: &mut A,
277        x: &str,
278        _: &mut Panel,
279    ) -> Transition<A> {
280        match x {
281            "Traffic simulation tutorial" => {
282                self.run(ctx, app, Executable::ABStreet, vec!["--tutorial-intro"])
283            }
284            "Traffic simulation challenges" => {
285                self.run(ctx, app, Executable::ABStreet, vec!["--challenges"])
286            }
287            "15-minute Santa" => self.run(ctx, app, Executable::Santa, vec![]),
288            "Traffic simulation sandbox" => {
289                self.run(ctx, app, Executable::ABStreet, vec!["--sandbox"])
290            }
291            "Community proposals" => self.run(ctx, app, Executable::ABStreet, vec!["--proposals"]),
292            "Ungap the Map" => self.run(ctx, app, Executable::ABStreet, vec!["--ungap"]),
293            "15-minute neighborhoods" => self.run(ctx, app, Executable::FifteenMin, vec![]),
294            "Low traffic neighborhoods" => self.run(ctx, app, Executable::LTN, vec![]),
295            "ActDev" => {
296                open_browser("https://actdev.cyipt.bike");
297                Transition::Keep
298            }
299            "Advanced tools" => self.run(ctx, app, Executable::ABStreet, vec!["--devtools"]),
300            "About" => Transition::Push(PopupMsg::new_state(
301                ctx,
302                "About A/B Street",
303                vec![
304                    "Disclaimer: This software is based on imperfect data, heuristics concocted",
305                    "under the influence of cold brew, a simplified traffic simulation model,",
306                    "and a deeply flawed understanding of how much articulated buses can bend",
307                    "around tight corners. Use this as a conversation starter with your city",
308                    "government, not a final decision maker. Any resemblance of in-game",
309                    "characters to real people is probably coincidental, unless of course you",
310                    "stumble across the elusive \"Dustin Bikelino\". Have the appropriate",
311                    "amount of fun.",
312                ],
313            )),
314            "Credits" => {
315                open_browser("https://a-b-street.github.io/docs/project/team.html");
316                Transition::Keep
317            }
318            "Download the new release" => {
319                open_browser("https://github.com/a-b-street/abstreet/releases");
320                Transition::Keep
321            }
322            _ => unreachable!(),
323        }
324    }
325}
326
327#[cfg(not(target_arch = "wasm32"))]
328#[allow(unused, clippy::logic_bug)]
329mod built_info {
330    use super::*;
331
332    include!(concat!(env!("OUT_DIR"), "/built.rs"));
333
334    pub fn maybe_update(ctx: &mut EventCtx) -> Widget {
335        let t = built::util::strptime(BUILT_TIME_UTC);
336
337        let txt = widgetry::Text::from(format!("This version built on {}", t.date_naive()))
338            .into_widget(ctx);
339        // Disable this warning; no promise about a release schedule anymore
340        if false && (chrono::Utc::now() - t).num_days() > 15 {
341            Widget::row(vec![
342                txt.centered_vert(),
343                ctx.style()
344                    .btn_outline
345                    .text("Download the new release")
346                    .build_def(ctx),
347            ])
348        } else {
349            txt
350        }
351    }
352}
353
354#[cfg(target_arch = "wasm32")]
355mod built_info {
356    use super::*;
357
358    pub fn maybe_update(_: &mut EventCtx) -> Widget {
359        Widget::nothing()
360    }
361}