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
8pub 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 Transition::Keep
173 }
174 }
175}
176
177impl Executable {
178 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 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 #[cfg(not(target_arch = "wasm32"))]
205 {
206 use std::process::Command;
207
208 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 #[cfg(not(windows))]
221 {
222 use std::os::unix::process::CommandExt;
223 let err = Command::new(binary).args(args).exec();
224 Transition::Push(PopupMsg::new_state(ctx, "Error", vec![err.to_string()]))
226 }
227
228 #[cfg(windows)]
231 {
232 abstutil::must_run_cmd(Command::new(binary).args(args));
233 Transition::Keep
234 }
235 }
236
237 #[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 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 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}