1 use crate::app::{App, FindDelayedIntersections, ShowEverything};
2 use crate::common::Warping;
3 use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
4 use crate::helpers::ID;
5 use crate::render::DrawOptions;
6 use crate::sandbox::{GameplayMode, SandboxMode};
7 use abstutil::prettyprint_usize;
8 use geom::{Duration, Polygon, Pt2D, Ring, Time};
9 use instant::Instant;
10 use sim::AlertLocation;
11 use widgetry::{
12     hotkey, AreaSlider, Btn, Checkbox, Choice, Color, EventCtx, GeomBatch, GfxCtx,
13     HorizontalAlignment, Key, Line, Outcome, Panel, PersistentSplit, RewriteColor, Text,
14     UpdateType, VerticalAlignment, Widget,
15 };
16 
17 pub struct SpeedControls {
18     pub panel: Panel,
19 
20     paused: bool,
21     setting: SpeedSetting,
22 }
23 
24 #[derive(Clone, Copy, PartialEq, PartialOrd)]
25 enum SpeedSetting {
26     // 1 sim second per real second
27     Realtime,
28     // 5 sim seconds per real second
29     Fast,
30     // 30 sim seconds per real second
31     Faster,
32     // 1 sim hour per real second
33     Fastest,
34 }
35 
36 impl SpeedControls {
make_panel(ctx: &mut EventCtx, app: &App, paused: bool, setting: SpeedSetting) -> Panel37     fn make_panel(ctx: &mut EventCtx, app: &App, paused: bool, setting: SpeedSetting) -> Panel {
38         let mut row = Vec::new();
39         row.push(
40             if paused {
41                 Btn::svg_def("system/assets/speed/triangle.svg").build(
42                     ctx,
43                     "play",
44                     hotkey(Key::Space),
45                 )
46             } else {
47                 Btn::svg_def("system/assets/speed/pause.svg").build(
48                     ctx,
49                     "pause",
50                     hotkey(Key::Space),
51                 )
52             }
53             .container()
54             .padding(9)
55             .bg(app.cs.section_bg)
56             .margin_right(16),
57         );
58 
59         row.push(
60             Widget::custom_row(
61                 vec![
62                     (SpeedSetting::Realtime, "real-time speed"),
63                     (SpeedSetting::Fast, "5x speed"),
64                     (SpeedSetting::Faster, "30x speed"),
65                     (SpeedSetting::Fastest, "3600x speed"),
66                 ]
67                 .into_iter()
68                 .map(|(s, label)| {
69                     let mut txt = Text::from(Line(label).small());
70                     txt.extend(Text::tooltip(ctx, hotkey(Key::LeftArrow), "slow down"));
71                     txt.extend(Text::tooltip(ctx, hotkey(Key::RightArrow), "speed up"));
72 
73                     GeomBatch::load_svg(ctx.prerender, "system/assets/speed/triangle.svg")
74                         .color(if setting >= s {
75                             RewriteColor::NoOp
76                         } else {
77                             RewriteColor::ChangeAll(Color::WHITE.alpha(0.2))
78                         })
79                         .to_btn(ctx)
80                         .tooltip(txt)
81                         .build(ctx, label, None)
82                         .margin_right(6)
83                 })
84                 .collect(),
85             )
86             .bg(app.cs.section_bg)
87             .centered()
88             .padding(6)
89             .margin_right(16),
90         );
91 
92         row.push(
93             PersistentSplit::new(
94                 ctx,
95                 "step forwards",
96                 app.opts.time_increment,
97                 hotkey(Key::M),
98                 vec![
99                     Choice::new("+1h", Duration::hours(1)),
100                     Choice::new("+30m", Duration::minutes(30)),
101                     Choice::new("+10m", Duration::minutes(10)),
102                     Choice::new("+0.1s", Duration::seconds(0.1)),
103                 ],
104             )
105             .bg(app.cs.section_bg)
106             .margin_right(16),
107         );
108 
109         row.push(
110             Widget::custom_row(vec![
111                 Btn::svg_def("system/assets/speed/jump_to_time.svg")
112                     .build(ctx, "jump to specific time", hotkey(Key::B))
113                     .container()
114                     .padding(9),
115                 Btn::svg_def("system/assets/speed/reset.svg")
116                     .build(ctx, "reset to midnight", hotkey(Key::X))
117                     .container()
118                     .padding(9),
119             ])
120             .bg(app.cs.section_bg),
121         );
122 
123         Panel::new(Widget::custom_row(row))
124             .aligned(
125                 HorizontalAlignment::Center,
126                 VerticalAlignment::BottomAboveOSD,
127             )
128             .build(ctx)
129     }
130 
new(ctx: &mut EventCtx, app: &App) -> SpeedControls131     pub fn new(ctx: &mut EventCtx, app: &App) -> SpeedControls {
132         let panel = SpeedControls::make_panel(ctx, app, false, SpeedSetting::Realtime);
133         SpeedControls {
134             panel,
135             paused: false,
136             setting: SpeedSetting::Realtime,
137         }
138     }
139 
event( &mut self, ctx: &mut EventCtx, app: &mut App, maybe_mode: Option<&GameplayMode>, ) -> Option<Transition>140     pub fn event(
141         &mut self,
142         ctx: &mut EventCtx,
143         app: &mut App,
144         maybe_mode: Option<&GameplayMode>,
145     ) -> Option<Transition> {
146         match self.panel.event(ctx) {
147             Outcome::Clicked(x) => match x.as_ref() {
148                 "real-time speed" => {
149                     self.setting = SpeedSetting::Realtime;
150                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
151                     return None;
152                 }
153                 "5x speed" => {
154                     self.setting = SpeedSetting::Fast;
155                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
156                     return None;
157                 }
158                 "30x speed" => {
159                     self.setting = SpeedSetting::Faster;
160                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
161                     return None;
162                 }
163                 "3600x speed" => {
164                     self.setting = SpeedSetting::Fastest;
165                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
166                     return None;
167                 }
168                 "play" => {
169                     self.paused = false;
170                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
171                     return None;
172                 }
173                 "pause" => {
174                     self.pause(ctx, app);
175                 }
176                 "reset to midnight" => {
177                     if let Some(mode) = maybe_mode {
178                         return Some(Transition::Replace(SandboxMode::new(
179                             ctx,
180                             app,
181                             mode.clone(),
182                         )));
183                     } else {
184                         return Some(Transition::Push(PopupMsg::new(
185                             ctx,
186                             "Error",
187                             vec!["Sorry, you can't go rewind time from this mode."],
188                         )));
189                     }
190                 }
191                 "jump to specific time" => {
192                     return Some(Transition::Push(Box::new(JumpToTime::new(
193                         ctx,
194                         app,
195                         maybe_mode.cloned(),
196                     ))));
197                 }
198                 "step forwards" => {
199                     let dt = self.panel.persistent_split_value("step forwards");
200                     if dt == Duration::seconds(0.1) {
201                         app.primary
202                             .sim
203                             .tiny_step(&app.primary.map, &mut app.primary.sim_cb);
204                         app.recalculate_current_selection(ctx);
205                         return Some(Transition::KeepWithMouseover);
206                     }
207                     return Some(Transition::Push(TimeWarpScreen::new(
208                         ctx,
209                         app,
210                         app.primary.sim.time() + dt,
211                         None,
212                     )));
213                 }
214                 _ => unreachable!(),
215             },
216             _ => {}
217         }
218         // Just kind of constantly scrape this
219         app.opts.time_increment = self.panel.persistent_split_value("step forwards");
220 
221         if ctx.input.key_pressed(Key::LeftArrow) {
222             match self.setting {
223                 SpeedSetting::Realtime => self.pause(ctx, app),
224                 SpeedSetting::Fast => {
225                     self.setting = SpeedSetting::Realtime;
226                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
227                 }
228                 SpeedSetting::Faster => {
229                     self.setting = SpeedSetting::Fast;
230                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
231                 }
232                 SpeedSetting::Fastest => {
233                     self.setting = SpeedSetting::Faster;
234                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
235                 }
236             }
237         }
238         if ctx.input.key_pressed(Key::RightArrow) {
239             match self.setting {
240                 SpeedSetting::Realtime => {
241                     if self.paused {
242                         self.paused = false;
243                         self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
244                     } else {
245                         self.setting = SpeedSetting::Fast;
246                         self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
247                     }
248                 }
249                 SpeedSetting::Fast => {
250                     self.setting = SpeedSetting::Faster;
251                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
252                 }
253                 SpeedSetting::Faster => {
254                     self.setting = SpeedSetting::Fastest;
255                     self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
256                 }
257                 SpeedSetting::Fastest => {}
258             }
259         }
260 
261         if !self.paused {
262             if let Some(real_dt) = ctx.input.nonblocking_is_update_event() {
263                 ctx.input.use_update_event();
264                 let multiplier = match self.setting {
265                     SpeedSetting::Realtime => 1.0,
266                     SpeedSetting::Fast => 5.0,
267                     SpeedSetting::Faster => 30.0,
268                     SpeedSetting::Fastest => 3600.0,
269                 };
270                 let dt = multiplier * real_dt;
271                 // TODO This should match the update frequency in widgetry. Plumb along the deadline
272                 // or frequency to here.
273                 app.primary.sim.time_limited_step(
274                     &app.primary.map,
275                     dt,
276                     Duration::seconds(0.033),
277                     &mut app.primary.sim_cb,
278                 );
279                 app.recalculate_current_selection(ctx);
280             }
281         }
282 
283         // TODO Need to do this anywhere that steps the sim, like TimeWarpScreen.
284         let alerts = app.primary.sim.clear_alerts();
285         if !alerts.is_empty() {
286             let popup = PopupMsg::new(
287                 ctx,
288                 "Alerts",
289                 alerts.iter().map(|(_, _, msg)| msg).collect(),
290             );
291             let maybe_id = match alerts[0].1 {
292                 AlertLocation::Nil => None,
293                 AlertLocation::Intersection(i) => Some(ID::Intersection(i)),
294                 // TODO Open info panel and warp to them
295                 AlertLocation::Person(_) => None,
296                 AlertLocation::Building(b) => Some(ID::Building(b)),
297             };
298             // TODO Can filter for particular alerts places like this:
299             /*if !alerts[0].2.contains("Turn conflict cycle") {
300                 return None;
301             }*/
302             /*if maybe_id != Some(ID::Building(map_model::BuildingID(91))) {
303                 return None;
304             }*/
305             self.pause(ctx, app);
306             if let Some(id) = maybe_id {
307                 // Just go to the first one, but print all messages
308                 return Some(Transition::Multi(vec![
309                     Transition::Push(popup),
310                     Transition::Push(Warping::new(
311                         ctx,
312                         id.canonical_point(&app.primary).unwrap(),
313                         Some(10.0),
314                         None,
315                         &mut app.primary,
316                     )),
317                 ]));
318             } else {
319                 return Some(Transition::Push(popup));
320             }
321         }
322 
323         None
324     }
325 
draw(&self, g: &mut GfxCtx)326     pub fn draw(&self, g: &mut GfxCtx) {
327         self.panel.draw(g);
328     }
329 
pause(&mut self, ctx: &mut EventCtx, app: &App)330     pub fn pause(&mut self, ctx: &mut EventCtx, app: &App) {
331         if !self.paused {
332             self.paused = true;
333             self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
334         }
335     }
336 
resume_realtime(&mut self, ctx: &mut EventCtx, app: &App)337     pub fn resume_realtime(&mut self, ctx: &mut EventCtx, app: &App) {
338         if self.paused || self.setting != SpeedSetting::Realtime {
339             self.paused = false;
340             self.setting = SpeedSetting::Realtime;
341             self.panel = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
342         }
343     }
344 
is_paused(&self) -> bool345     pub fn is_paused(&self) -> bool {
346         self.paused
347     }
348 }
349 
350 // TODO Text entry would be great
351 struct JumpToTime {
352     panel: Panel,
353     target: Time,
354     halt_limit: Duration,
355     maybe_mode: Option<GameplayMode>,
356 }
357 
358 impl JumpToTime {
new(ctx: &mut EventCtx, app: &App, maybe_mode: Option<GameplayMode>) -> JumpToTime359     fn new(ctx: &mut EventCtx, app: &App, maybe_mode: Option<GameplayMode>) -> JumpToTime {
360         let target = app.primary.sim.time();
361         let end_of_day = app.primary.sim.get_end_of_day();
362         let halt_limit = app.opts.time_warp_halt_limit;
363         JumpToTime {
364             target,
365             halt_limit,
366             maybe_mode,
367             panel: Panel::new(Widget::col(vec![
368                 Widget::row(vec![
369                     Line("Jump to what time?").small_heading().draw(ctx),
370                     Btn::plaintext("X")
371                         .build(ctx, "close", hotkey(Key::Escape))
372                         .align_right(),
373                 ]),
374                 Checkbox::checkbox(
375                     ctx,
376                     "skip drawing (for faster simulations)",
377                     None,
378                     app.opts.dont_draw_time_warp,
379                 )
380                 .margin_above(30)
381                 .named("don't draw"),
382                 Widget::horiz_separator(ctx, 0.25).margin_above(10),
383                 if app.has_prebaked().is_some() {
384                     Widget::draw_batch(
385                         ctx,
386                         GeomBatch::from(vec![(
387                             Color::WHITE.alpha(0.7),
388                             area_under_curve(
389                                 app.prebaked().active_agents(end_of_day),
390                                 // TODO Auto fill width
391                                 500.0,
392                                 50.0,
393                             ),
394                         )]),
395                     )
396                 } else {
397                     Widget::nothing()
398                 },
399                 // TODO Auto-fill width?
400                 AreaSlider::new(
401                     ctx,
402                     0.25 * ctx.canvas.window_width,
403                     target.to_percent(end_of_day).min(1.0),
404                 )
405                 .named("time slider")
406                 .centered_horiz()
407                 // EZGUI FIXME: margin_below having no effect here, so instead we add a margin_top
408                 // to the subsequent element
409                 //.margin_above(16).margin_below(16),
410                 .margin_above(16),
411                 build_jump_to_time_btn(target, ctx),
412                 Widget::horiz_separator(ctx, 0.25).margin_above(16),
413                 build_jump_to_delay_picker(halt_limit, ctx).margin_above(16),
414                 build_jump_to_delay_button(halt_limit, ctx),
415             ]))
416             .build(ctx),
417         }
418     }
419 }
420 
421 impl State for JumpToTime {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition422     fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
423         match self.panel.event(ctx) {
424             Outcome::Clicked(x) => match x.as_ref() {
425                 "close" => {
426                     return Transition::Pop;
427                 }
428                 "jump to time" => {
429                     if self.target < app.primary.sim.time() {
430                         if let Some(mode) = self.maybe_mode.take() {
431                             return Transition::Multi(vec![
432                                 Transition::Replace(SandboxMode::new(ctx, app, mode)),
433                                 Transition::Push(TimeWarpScreen::new(ctx, app, self.target, None)),
434                             ]);
435                         } else {
436                             return Transition::Replace(PopupMsg::new(
437                                 ctx,
438                                 "Error",
439                                 vec!["Sorry, you can't go rewind time from this mode."],
440                             ));
441                         }
442                     }
443                     return Transition::Replace(TimeWarpScreen::new(ctx, app, self.target, None));
444                 }
445                 "choose delay" => return Transition::Keep,
446                 "jump to delay" => {
447                     let halt_limit = self.panel.persistent_split_value("choose delay");
448                     app.opts.time_warp_halt_limit = halt_limit;
449                     return Transition::Replace(TimeWarpScreen::new(
450                         ctx,
451                         app,
452                         app.primary.sim.get_end_of_day(),
453                         Some(halt_limit),
454                     ));
455                 }
456                 _ => unreachable!(),
457             },
458             Outcome::Changed => {
459                 app.opts.dont_draw_time_warp = self.panel.is_checked("don't draw");
460             }
461             _ => {}
462         }
463         let target = app
464             .primary
465             .sim
466             .get_end_of_day()
467             .percent_of(self.panel.area_slider("time slider").get_percent())
468             .round_seconds(600.0);
469         if target != self.target {
470             self.target = target;
471             self.panel
472                 .replace(ctx, "jump to time", build_jump_to_time_btn(target, ctx));
473         }
474 
475         let halt_limit = self.panel.persistent_split_value("choose delay");
476         if halt_limit != self.halt_limit {
477             self.halt_limit = halt_limit;
478             self.panel.replace(
479                 ctx,
480                 "jump to delay",
481                 build_jump_to_delay_button(halt_limit, ctx),
482             );
483         }
484 
485         if self.panel.clicked_outside(ctx) {
486             return Transition::Pop;
487         }
488 
489         Transition::Keep
490     }
491 
draw(&self, g: &mut GfxCtx, app: &App)492     fn draw(&self, g: &mut GfxCtx, app: &App) {
493         State::grey_out_map(g, app);
494         self.panel.draw(g);
495     }
496 }
497 
498 // Display a nicer screen for jumping forwards in time, allowing cancellation.
499 pub struct TimeWarpScreen {
500     target: Time,
501     wall_time_started: Instant,
502     sim_time_started: geom::Time,
503     halt_upon_delay: Option<Duration>,
504     panel: Panel,
505 }
506 
507 impl TimeWarpScreen {
new( ctx: &mut EventCtx, app: &mut App, target: Time, mut halt_upon_delay: Option<Duration>, ) -> Box<dyn State>508     pub fn new(
509         ctx: &mut EventCtx,
510         app: &mut App,
511         target: Time,
512         mut halt_upon_delay: Option<Duration>,
513     ) -> Box<dyn State> {
514         if let Some(halt_limit) = halt_upon_delay {
515             if app.primary.sim_cb.is_none() {
516                 app.primary.sim_cb = Some(Box::new(FindDelayedIntersections {
517                     halt_limit,
518                     report_limit: halt_limit,
519                     currently_delayed: Vec::new(),
520                 }));
521                 // TODO Can we get away with less frequently? Not sure about all the edge cases
522                 app.primary.sim.set_periodic_callback(Duration::minutes(1));
523             } else {
524                 halt_upon_delay = None;
525             }
526         }
527 
528         Box::new(TimeWarpScreen {
529             target,
530             wall_time_started: Instant::now(),
531             sim_time_started: app.primary.sim.time(),
532             halt_upon_delay,
533             panel: Panel::new(
534                 Widget::col(vec![
535                     Text::new().draw(ctx).named("text"),
536                     Btn::text_bg2("stop now")
537                         .build_def(ctx, hotkey(Key::Escape))
538                         .centered_horiz(),
539                 ])
540                 // hardcoded width avoids jiggle due to text updates
541                 .force_width(700.0),
542             )
543             .build(ctx),
544         })
545     }
546 }
547 
548 impl State for TimeWarpScreen {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition549     fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
550         if ctx.input.nonblocking_is_update_event().is_some() {
551             ctx.input.use_update_event();
552             app.primary.sim.time_limited_step(
553                 &app.primary.map,
554                 self.target - app.primary.sim.time(),
555                 Duration::seconds(0.033),
556                 &mut app.primary.sim_cb,
557             );
558             for (t, maybe_i, alert) in app.primary.sim.clear_alerts() {
559                 // TODO Just the first :(
560                 return Transition::Replace(PopupMsg::new(
561                     ctx,
562                     "Alert",
563                     vec![format!("At {}, near {:?}, {}", t, maybe_i, alert)],
564                 ));
565             }
566             if let Some(ref mut cb) = app.primary.sim_cb {
567                 let di = cb.downcast_mut::<FindDelayedIntersections>().unwrap();
568                 if let Some((i, t)) = di.currently_delayed.get(0) {
569                     if app.primary.sim.time() - *t > di.halt_limit {
570                         let id = ID::Intersection(*i);
571                         app.layer =
572                             Some(Box::new(crate::layer::traffic::TrafficJams::new(ctx, app)));
573                         return Transition::Replace(Warping::new(
574                             ctx,
575                             id.canonical_point(&app.primary).unwrap(),
576                             Some(10.0),
577                             Some(id),
578                             &mut app.primary,
579                         ));
580                     }
581                 }
582             }
583 
584             let now = app.primary.sim.time();
585             let (finished_after, _) = app.primary.sim.num_trips();
586             let finished_before = if app.has_prebaked().is_some() {
587                 let mut cnt = 0;
588                 for (t, _, _, _) in &app.prebaked().finished_trips {
589                     if *t > now {
590                         break;
591                     }
592                     cnt += 1;
593                 }
594                 Some(cnt)
595             } else {
596                 None
597             };
598 
599             let elapsed_sim_time = now - self.sim_time_started;
600             let elapsed_wall_time = Duration::realtime_elapsed(self.wall_time_started);
601             let txt = Text::from_multiline(vec![
602                 // I'm covered in shame for not doing this from the start.
603                 Line("Let's do the time warp again!").small_heading(),
604                 Line(format!(
605                     "{} / {}",
606                     now.ampm_tostring(),
607                     self.target.ampm_tostring()
608                 )),
609                 Line(format!(
610                     "Speed: {}x",
611                     prettyprint_usize((elapsed_sim_time / elapsed_wall_time) as usize)
612                 )),
613                 if let Some(n) = finished_before {
614                     // TODO Underline
615                     Line(format!(
616                         "Finished trips: {} ({} compared to before \"{}\")",
617                         prettyprint_usize(finished_after),
618                         compare_count(finished_after, n),
619                         app.primary.map.get_edits().edits_name,
620                     ))
621                 } else {
622                     Line(format!(
623                         "Finished trips: {}",
624                         prettyprint_usize(finished_after)
625                     ))
626                 },
627             ]);
628 
629             self.panel.replace(ctx, "text", txt.draw(ctx).named("text"));
630         }
631         if app.primary.sim.time() == self.target {
632             return Transition::Pop;
633         }
634 
635         match self.panel.event(ctx) {
636             Outcome::Clicked(x) => match x.as_ref() {
637                 "stop now" => {
638                     return Transition::Pop;
639                 }
640                 _ => unreachable!(),
641             },
642             _ => {}
643         }
644         if self.panel.clicked_outside(ctx) {
645             return Transition::Pop;
646         }
647 
648         ctx.request_update(UpdateType::Game);
649         Transition::Keep
650     }
651 
draw_baselayer(&self) -> DrawBaselayer652     fn draw_baselayer(&self) -> DrawBaselayer {
653         DrawBaselayer::Custom
654     }
655 
draw(&self, g: &mut GfxCtx, app: &App)656     fn draw(&self, g: &mut GfxCtx, app: &App) {
657         if app.opts.dont_draw_time_warp {
658             g.clear(app.cs.section_bg);
659         } else {
660             app.draw(
661                 g,
662                 DrawOptions::new(),
663                 &app.primary.sim,
664                 &ShowEverything::new(),
665             );
666             State::grey_out_map(g, app);
667         }
668 
669         self.panel.draw(g);
670     }
671 
on_destroy(&mut self, _: &mut EventCtx, app: &mut App)672     fn on_destroy(&mut self, _: &mut EventCtx, app: &mut App) {
673         if self.halt_upon_delay.is_some() {
674             assert!(app.primary.sim_cb.is_some());
675             app.primary.sim_cb = None;
676             app.primary.sim.unset_periodic_callback();
677         }
678     }
679 }
680 
681 pub struct TimePanel {
682     time: Time,
683     pub panel: Panel,
684 }
685 
686 impl TimePanel {
new(ctx: &mut EventCtx, app: &App) -> TimePanel687     pub fn new(ctx: &mut EventCtx, app: &App) -> TimePanel {
688         TimePanel {
689             time: app.primary.sim.time(),
690             panel: Panel::new(Widget::col(vec![
691                 Text::from(Line(app.primary.sim.time().ampm_tostring()).big_monospaced())
692                     .draw(ctx)
693                     .centered_horiz(),
694                 {
695                     let mut batch = GeomBatch::new();
696                     // This is manually tuned
697                     let width = 300.0;
698                     let height = 15.0;
699                     // Just clamp if we simulate past the expected end
700                     let percent = app
701                         .primary
702                         .sim
703                         .time()
704                         .to_percent(app.primary.sim.get_end_of_day())
705                         .min(1.0);
706 
707                     // TODO Why is the rounding so hard? The white background is always rounded
708                     // at both ends. The moving bar should always be rounded on the left, flat
709                     // on the right, except at the very end (for the last 'radius' pixels). And
710                     // when the width is too small for the radius, this messes up.
711 
712                     batch.push(Color::WHITE, Polygon::rectangle(width, height));
713 
714                     if percent != 0.0 {
715                         batch.push(
716                             if percent < 0.25 || percent > 0.75 {
717                                 app.cs.night_time_slider
718                             } else {
719                                 app.cs.day_time_slider
720                             },
721                             Polygon::rectangle(percent * width, height),
722                         );
723                     }
724 
725                     Widget::draw_batch(ctx, batch)
726                 },
727                 Widget::custom_row(vec![
728                     Line("00:00").small_monospaced().draw(ctx),
729                     Widget::draw_svg(ctx, "system/assets/speed/sunrise.svg"),
730                     Line("12:00").small_monospaced().draw(ctx),
731                     Widget::draw_svg(ctx, "system/assets/speed/sunset.svg"),
732                     Line("24:00").small_monospaced().draw(ctx),
733                 ])
734                 .evenly_spaced(),
735             ]))
736             .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
737             .build(ctx),
738         }
739     }
740 
event(&mut self, ctx: &mut EventCtx, app: &mut App)741     pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App) {
742         if self.time != app.primary.sim.time() {
743             *self = TimePanel::new(ctx, app);
744         }
745         self.panel.event(ctx);
746     }
747 
draw(&self, g: &mut GfxCtx)748     pub fn draw(&self, g: &mut GfxCtx) {
749         self.panel.draw(g);
750     }
751 }
752 
area_under_curve(raw: Vec<(Time, usize)>, width: f64, height: f64) -> Polygon753 fn area_under_curve(raw: Vec<(Time, usize)>, width: f64, height: f64) -> Polygon {
754     assert!(!raw.is_empty());
755     let min_x = Time::START_OF_DAY;
756     let min_y = 0;
757     let max_x = raw.last().unwrap().0;
758     let max_y = raw.iter().max_by_key(|(_, cnt)| *cnt).unwrap().1;
759 
760     let mut pts = Vec::new();
761     for (t, cnt) in raw {
762         pts.push(lttb::DataPoint::new(
763             width * (t - min_x) / (max_x - min_x),
764             height * (1.0 - (((cnt - min_y) as f64) / ((max_y - min_y) as f64))),
765         ));
766     }
767     let mut downsampled = Vec::new();
768     for pt in lttb::lttb(pts, 100) {
769         downsampled.push(Pt2D::new(pt.x, pt.y));
770     }
771     downsampled.push(Pt2D::new(width, height));
772     downsampled.push(downsampled[0]);
773     Ring::must_new(downsampled).to_polygon()
774 }
775 
776 // TODO Maybe color, put in helpers
compare_count(after: usize, before: usize) -> String777 fn compare_count(after: usize, before: usize) -> String {
778     if after == before {
779         "+0".to_string()
780     } else if after > before {
781         format!("+{}", prettyprint_usize(after - before))
782     } else {
783         format!("-{}", prettyprint_usize(before - after))
784     }
785 }
786 
build_jump_to_time_btn(target: Time, ctx: &EventCtx) -> Widget787 fn build_jump_to_time_btn(target: Time, ctx: &EventCtx) -> Widget {
788     Btn::text_bg2(format!("Jump to {}", target.ampm_tostring()))
789         .build(ctx, "jump to time", hotkey(Key::Enter))
790         .named("jump to time")
791         .centered_horiz()
792         .margin_above(16)
793 }
794 
build_jump_to_delay_button(halt_limit: Duration, ctx: &EventCtx) -> Widget795 fn build_jump_to_delay_button(halt_limit: Duration, ctx: &EventCtx) -> Widget {
796     Btn::text_bg2(format!("Jump to next {} delay", halt_limit))
797         .build(ctx, "jump to delay", hotkey(Key::D))
798         .named("jump to delay")
799         .centered_horiz()
800         .margin_above(16)
801 }
802 
build_jump_to_delay_picker(halt_limit: Duration, ctx: &EventCtx) -> Widget803 fn build_jump_to_delay_picker(halt_limit: Duration, ctx: &EventCtx) -> Widget {
804     // EZGUI TODO: it'd be nice if we could style the fg color for persistent splits but this needs
805     // to be passed into the button builder in init. so either we'd need to make a required
806     // argument, or introduce a persistentsplitbuilder, or re-work splitbuilder to allow mutating
807     // the color after the fact which requires holding more state to re-invoke the btnbuilder
808     PersistentSplit::new(
809         ctx,
810         "choose delay",
811         halt_limit,
812         None,
813         vec![
814             Choice::new("1 minute delay", Duration::minutes(1)),
815             Choice::new("2 minute delay", Duration::minutes(2)),
816             Choice::new("5 minute delay", Duration::minutes(5)),
817             Choice::new("10 minute delay", Duration::minutes(10)),
818         ],
819     )
820     .outline(2.0, Color::WHITE)
821     .centered_horiz()
822 }
823