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