1use alloc::boxed::Box;
5use alloc::vec::Vec;
6
7use retroglyph_core::app::{App, Flow, Frame};
8use retroglyph_core::backend::Backend;
9use retroglyph_core::event::{Event, KeyCode, KeyEventKind};
10use retroglyph_core::frames::FrameStats;
11use retroglyph_core::grid::{HasSize, Rect, Size};
12use retroglyph_core::terminal::Terminal;
13
14use super::mode::PerfOverlayMode;
15use super::renderer::{DefaultPerfRenderer, PerfRenderer};
16use super::{DEFAULT_LAYER, FRAME_HISTORY};
17use crate::Surface;
18
19#[must_use]
25pub fn default_is_toggle_key(event: &Event) -> bool {
26 let Event::Key(key) = event else {
27 return false;
28 };
29 key.kind == KeyEventKind::Press && matches!(key.code, KeyCode::Char('`') | KeyCode::F(1))
30}
31
32struct FullMode {
39 renderer: Box<dyn PerfRenderer>,
40 size: Size,
41}
42
43pub struct PerfOverlayApp<A, R = DefaultPerfRenderer> {
45 inner: A,
46 stats: FrameStats<FRAME_HISTORY>,
47 backend: &'static str,
48 renderer: R,
49 mode: PerfOverlayMode,
50 full: Option<FullMode>,
51 layer: u8,
52 size: Size,
53 toggle_key: fn(&Event) -> bool,
54 passthrough: Vec<Event>,
58}
59
60impl<A> PerfOverlayApp<A, DefaultPerfRenderer> {
61 #[must_use]
65 pub fn new(inner: A, backend: &'static str) -> Self {
66 Self::with_renderer(inner, backend, DefaultPerfRenderer::new())
67 }
68}
69
70impl<A, F> PerfOverlayApp<A, F>
71where
72 F: FnMut(&FrameStats<FRAME_HISTORY>, &str, Rect, &mut Surface<'_>),
73{
74 #[must_use]
82 pub fn with_closure(inner: A, backend: &'static str, renderer: F) -> Self {
83 Self::with_renderer(inner, backend, renderer)
84 }
85}
86
87impl<A, R: PerfRenderer> PerfOverlayApp<A, R> {
88 #[must_use]
99 pub fn with_renderer(inner: A, backend: &'static str, renderer: R) -> Self {
100 Self {
101 inner,
102 stats: FrameStats::new(),
103 backend,
104 renderer,
105 mode: PerfOverlayMode::Compact,
106 full: None,
107 layer: DEFAULT_LAYER,
108 size: Size::new(64, 1),
109 toggle_key: default_is_toggle_key,
110 passthrough: Vec::new(),
111 }
112 }
113
114 #[must_use]
123 pub fn cycle_with<F>(mut self, size: Size, renderer: F) -> Self
124 where
125 F: FnMut(&FrameStats<FRAME_HISTORY>, &str, Rect, &mut Surface<'_>) + 'static,
126 {
127 self.full = Some(FullMode {
131 renderer: Box::new(renderer),
132 size,
133 });
134 self
135 }
136
137 #[must_use]
142 pub const fn visible(mut self, visible: bool) -> Self {
143 self.mode = if visible {
144 PerfOverlayMode::Compact
145 } else {
146 PerfOverlayMode::Off
147 };
148 self
149 }
150
151 #[must_use]
155 pub const fn layer(mut self, layer: u8) -> Self {
156 self.layer = layer;
157 self
158 }
159
160 #[must_use]
167 pub const fn size(mut self, size: Size) -> Self {
168 self.size = size;
169 self
170 }
171
172 #[must_use]
175 pub const fn toggle_key(mut self, matches: fn(&Event) -> bool) -> Self {
176 self.toggle_key = matches;
177 self
178 }
179
180 #[must_use]
182 pub const fn stats(&self) -> &FrameStats<FRAME_HISTORY> {
183 &self.stats
184 }
185
186 #[must_use]
190 pub const fn is_visible(&self) -> bool {
191 !matches!(self.mode, PerfOverlayMode::Off)
192 }
193
194 #[must_use]
196 pub const fn mode(&self) -> PerfOverlayMode {
197 self.mode
198 }
199
200 pub const fn set_mode(&mut self, mode: PerfOverlayMode) {
206 self.mode = mode;
207 }
208
209 pub const fn toggle(&mut self) {
213 self.mode = self.mode.next(self.full.is_some());
214 }
215
216 #[must_use]
218 pub const fn inner(&self) -> &A {
219 &self.inner
220 }
221
222 pub const fn inner_mut(&mut self) -> &mut A {
224 &mut self.inner
225 }
226
227 #[must_use]
230 pub fn into_inner(self) -> A {
231 self.inner
232 }
233
234 fn area(size: Size, term_width: u16, term_height: u16) -> Rect {
237 let width = size.width().min(term_width);
238 let height = size.height().min(term_height);
239 Rect::new(term_width - width, 0, width, height)
240 }
241}
242
243impl<B, A, R> App<B> for PerfOverlayApp<A, R>
244where
245 B: Backend,
246 A: App<B>,
247 R: PerfRenderer,
248{
249 fn update(&mut self, term: &mut Terminal<B>, frame: &Frame) -> Flow {
250 self.stats.record(frame.delta);
251
252 let mut toggled = false;
259 self.passthrough.clear();
260 for event in term.drain_events() {
261 if (self.toggle_key)(&event) {
262 toggled = true;
263 } else {
264 self.passthrough.push(event);
265 }
266 }
267 if toggled {
268 self.mode = self.mode.next(self.full.is_some());
269 }
270 term.requeue_events(self.passthrough.drain(..));
271
272 let mut flow = self.inner.update(term, frame);
273 if toggled && flow == Flow::Idle {
277 flow = Flow::Continue;
278 }
279
280 let drawn = match self.mode {
283 PerfOverlayMode::Off => None,
284 PerfOverlayMode::Compact => Some((self.size, None)),
285 PerfOverlayMode::Full => self.full.as_mut().map(|full| (full.size, Some(full))),
286 };
287 if let Some((size, full)) = drawn {
288 let (term_width, term_height) = {
289 let surface = term.surface();
290 (surface.width(), surface.height())
291 };
292 let area = Self::area(size, term_width, term_height);
293 if area.width() > 0 && area.height() > 0 {
294 let mut base = term.surface();
295 let mut surface = base.on_layer(self.layer);
296 match full {
297 None => self
298 .renderer
299 .render(&self.stats, self.backend, area, &mut surface),
300 Some(full) => {
301 full.renderer
302 .render(&self.stats, self.backend, area, &mut surface);
303 }
304 }
305 }
306 }
307
308 flow
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use retroglyph_core::backend::{Cursor, Headless, Input, Output};
315 use retroglyph_core::color::Style;
316 use retroglyph_core::event::{KeyEvent, KeyModifiers};
317 use retroglyph_core::grid::Pos;
318
319 use super::*;
320
321 struct CountingApp {
322 updates: u32,
323 exit_at: u64,
324 }
325
326 impl<B: Backend> App<B> for CountingApp {
327 fn update(&mut self, _term: &mut Terminal<B>, frame: &Frame) -> Flow {
328 self.updates += 1;
329 if frame.frame >= self.exit_at {
330 Flow::Exit
331 } else {
332 Flow::Continue
333 }
334 }
335 }
336
337 fn frame(n: u64) -> Frame {
338 Frame {
339 delta: core::time::Duration::from_millis(16),
340 frame: n,
341 }
342 }
343
344 struct NoPushEventBackend(Headless);
349
350 impl Output for NoPushEventBackend {
351 type Error = <Headless as Output>::Error;
352
353 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
354 where
355 I: Iterator<Item = retroglyph_core::backend::DrawCell<'a>>,
356 {
357 self.0.draw_layers(content)
358 }
359
360 fn resize(&mut self, size: Size) {
361 self.0.resize(size);
362 }
363
364 fn flush(&mut self) -> Result<(), Self::Error> {
365 self.0.flush()
366 }
367
368 fn size(&self) -> Size {
369 self.0.size()
370 }
371
372 fn clear(&mut self) -> Result<(), Self::Error> {
373 self.0.clear()
374 }
375 }
376
377 impl Input for NoPushEventBackend {
378 fn poll_event(&mut self, timeout: core::time::Duration) -> Option<Event> {
379 self.0.poll_event(timeout)
380 }
381 }
385
386 impl Cursor for NoPushEventBackend {
387 fn set_cursor_visible(&mut self, visible: bool) {
388 self.0.set_cursor_visible(visible);
389 }
390
391 fn set_cursor_position(&mut self, position: Pos) {
392 self.0.set_cursor_position(position);
393 }
394 }
395
396 struct SeesInput {
397 saw: bool,
398 }
399
400 impl<B: Backend> App<B> for SeesInput {
401 fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
402 if term.drain_events().next().is_some() {
403 self.saw = true;
404 }
405 Flow::Continue
406 }
407 }
408
409 #[test]
410 fn perf_overlay_passes_input_through_on_a_backend_with_the_default_push_event() {
411 let mut term = Terminal::new(NoPushEventBackend(Headless::new(40, 5)));
412 term.backend_mut().0.push_event(Event::Key(KeyEvent::new(
413 KeyCode::Char('q'),
414 KeyModifiers::NONE,
415 )));
416
417 let mut overlay = PerfOverlayApp::new(SeesInput { saw: false }, "custom");
418 App::update(&mut overlay, &mut term, &frame(0));
419
420 assert!(overlay.inner().saw);
421 }
422
423 #[test]
424 fn default_is_toggle_key_matches_backtick_and_f1_presses_only() {
425 for code in [KeyCode::Char('`'), KeyCode::F(1)] {
426 let press = Event::Key(KeyEvent::new(code, KeyModifiers::NONE));
427 assert!(default_is_toggle_key(&press));
428 }
429 let release = Event::Key(KeyEvent::with_kind(
430 KeyCode::Char('`'),
431 KeyModifiers::NONE,
432 KeyEventKind::Release,
433 ));
434 assert!(!default_is_toggle_key(&release));
435 assert!(!default_is_toggle_key(&Event::Close));
436 let other = Event::Key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE));
437 assert!(!default_is_toggle_key(&other));
438 }
439
440 #[test]
441 fn forwards_updates_and_records_frame_stats() {
442 let mut term = Terminal::new(Headless::new(40, 5));
443 let mut overlay = PerfOverlayApp::new(
444 CountingApp {
445 updates: 0,
446 exit_at: 2,
447 },
448 "headless",
449 );
450
451 for n in 0..3 {
452 let flow = App::update(&mut overlay, &mut term, &frame(n));
453 if flow == Flow::Exit {
454 break;
455 }
456 }
457
458 assert_eq!(overlay.inner().updates, 3);
459 assert_eq!(overlay.stats().frame_count(), 3);
460 }
461
462 #[test]
463 fn swallows_toggle_key_and_passes_other_input_through() {
464 let mut term = Terminal::new(Headless::new(40, 5));
465 term.backend_mut().push_event(Event::Key(KeyEvent::new(
466 KeyCode::Char('`'),
467 KeyModifiers::NONE,
468 )));
469 term.backend_mut().push_event(Event::Key(KeyEvent::new(
470 KeyCode::Char('q'),
471 KeyModifiers::NONE,
472 )));
473
474 let mut overlay = PerfOverlayApp::new(
475 CountingApp {
476 updates: 0,
477 exit_at: 100,
478 },
479 "headless",
480 );
481 assert!(overlay.is_visible());
482 let _ = App::update(&mut overlay, &mut term, &frame(0));
483 assert!(!overlay.is_visible(), "backtick should have toggled off");
484
485 let remaining: Vec<Event> = term.drain_events().collect();
487 assert_eq!(
488 remaining,
489 alloc::vec![Event::Key(KeyEvent::new(
490 KeyCode::Char('q'),
491 KeyModifiers::NONE
492 ))]
493 );
494 }
495
496 #[test]
497 fn toggling_while_idle_upgrades_flow_to_continue() {
498 struct AlwaysIdle;
499 impl<B: Backend> App<B> for AlwaysIdle {
500 fn update(&mut self, _term: &mut Terminal<B>, _frame: &Frame) -> Flow {
501 Flow::Idle
502 }
503 }
504
505 let mut term = Terminal::new(Headless::new(40, 5));
506 term.backend_mut().push_event(Event::Key(KeyEvent::new(
507 KeyCode::Char('`'),
508 KeyModifiers::NONE,
509 )));
510 let mut overlay = PerfOverlayApp::new(AlwaysIdle, "headless");
511
512 let flow = App::update(&mut overlay, &mut term, &frame(0));
513 assert_eq!(
514 flow,
515 Flow::Continue,
516 "toggle should upgrade Idle to Continue"
517 );
518
519 let flow = App::update(&mut overlay, &mut term, &frame(1));
521 assert_eq!(flow, Flow::Idle);
522 }
523
524 #[test]
525 fn layer_and_size_builders_set_their_fields() {
526 let overlay = PerfOverlayApp::new(
527 CountingApp {
528 updates: 0,
529 exit_at: 100,
530 },
531 "headless",
532 )
533 .layer(5)
534 .size(Size::new(10, 2));
535
536 assert_eq!(overlay.layer, 5);
537 assert_eq!(overlay.size, Size::new(10, 2));
538 }
539
540 #[test]
541 fn visible_true_is_explicitly_compact() {
542 let overlay = PerfOverlayApp::new(
546 CountingApp {
547 updates: 0,
548 exit_at: 100,
549 },
550 "headless",
551 )
552 .visible(true);
553
554 assert_eq!(overlay.mode(), PerfOverlayMode::Compact);
555 }
556
557 #[test]
558 fn zero_size_compact_area_draws_nothing() {
559 let mut term = Terminal::new(Headless::new(40, 5));
560 let mut overlay = PerfOverlayApp::new(
561 CountingApp {
562 updates: 0,
563 exit_at: 100,
564 },
565 "headless",
566 )
567 .size(Size::new(0, 0));
568
569 let _ = App::update(&mut overlay, &mut term, &frame(0));
570 term.present().expect("present");
571 assert!(
572 !term.backend().format_view().contains("fps"),
573 "a zero-size compact area should draw nothing"
574 );
575 }
576
577 #[test]
578 fn hidden_overlay_draws_nothing() {
579 let mut term = Terminal::new(Headless::new(40, 5));
580 let mut overlay = PerfOverlayApp::new(
581 CountingApp {
582 updates: 0,
583 exit_at: 100,
584 },
585 "headless",
586 )
587 .visible(false);
588
589 let _ = App::update(&mut overlay, &mut term, &frame(0));
590 term.present().expect("present");
591 assert!(
592 !term.backend().format_view().contains("fps"),
593 "hidden overlay should not draw"
594 );
595 }
596
597 #[test]
598 fn visible_overlay_draws_a_readout_after_the_first_frame() {
599 let mut term = Terminal::new(Headless::new(80, 5));
600 let mut overlay = PerfOverlayApp::new(
601 CountingApp {
602 updates: 0,
603 exit_at: 100,
604 },
605 "headless",
606 );
607
608 let _ = App::update(&mut overlay, &mut term, &frame(0));
609 term.present().expect("present");
610 let view = term.backend().format_view();
611 assert!(view.contains("fps"), "{view}");
612 assert!(view.contains("headless"), "{view}");
613 }
614
615 #[test]
616 fn custom_renderer_closure_is_used_instead_of_the_default() {
617 let mut term = Terminal::new(Headless::new(40, 5));
618 let mut overlay = PerfOverlayApp::with_closure(
619 CountingApp {
620 updates: 0,
621 exit_at: 100,
622 },
623 "headless",
624 |_stats, backend, area, surface| {
625 surface.print((area.left(), area.top()), backend, Style::new());
626 },
627 );
628
629 let _ = App::update(&mut overlay, &mut term, &frame(0));
630 term.present().expect("present");
631 let view = term.backend().format_view();
632 assert!(view.contains("headless"), "{view}");
633 assert!(
634 !view.contains("fps"),
635 "custom renderer should replace the default, {view}"
636 );
637 }
638
639 #[test]
640 fn without_cycle_with_toggle_only_visits_off_and_compact() {
641 let mut overlay = PerfOverlayApp::new(
642 CountingApp {
643 updates: 0,
644 exit_at: 100,
645 },
646 "headless",
647 );
648 assert_eq!(overlay.mode(), PerfOverlayMode::Compact);
649 overlay.toggle();
650 assert_eq!(overlay.mode(), PerfOverlayMode::Off);
651 overlay.toggle();
652 assert_eq!(overlay.mode(), PerfOverlayMode::Compact);
653 }
654
655 #[test]
656 fn cycle_with_makes_toggle_visit_off_compact_full() {
657 let mut overlay = PerfOverlayApp::new(
658 CountingApp {
659 updates: 0,
660 exit_at: 100,
661 },
662 "headless",
663 )
664 .cycle_with(Size::new(40, 6), |_stats, _backend, _area, _surface| {});
665
666 assert_eq!(overlay.mode(), PerfOverlayMode::Compact, "starts visible");
667 overlay.toggle();
668 assert_eq!(overlay.mode(), PerfOverlayMode::Full);
669 overlay.toggle();
670 assert_eq!(overlay.mode(), PerfOverlayMode::Off);
671 overlay.toggle();
672 assert_eq!(overlay.mode(), PerfOverlayMode::Compact, "cycle repeats");
673 }
674
675 #[test]
676 fn set_mode_jumps_directly_to_full() {
677 let mut overlay = PerfOverlayApp::new(
678 CountingApp {
679 updates: 0,
680 exit_at: 100,
681 },
682 "headless",
683 )
684 .cycle_with(Size::new(40, 6), |_stats, _backend, _area, _surface| {});
685
686 overlay.set_mode(PerfOverlayMode::Full);
687 assert_eq!(overlay.mode(), PerfOverlayMode::Full);
688 assert!(overlay.is_visible());
689 }
690
691 #[test]
692 fn full_mode_draws_with_its_own_renderer_at_its_own_size() {
693 let mut term = Terminal::new(Headless::new(60, 10));
694 let mut overlay = PerfOverlayApp::new(
695 CountingApp {
696 updates: 0,
697 exit_at: 100,
698 },
699 "headless",
700 )
701 .cycle_with(Size::new(30, 5), |_stats, _backend, area, surface| {
702 surface.print((area.left(), area.top()), "FULLMODE", Style::new());
705 });
706
707 let _ = App::update(&mut overlay, &mut term, &frame(0));
709 term.present().expect("present");
710 let view = term.backend().format_view();
711 assert!(view.contains("fps"), "{view}");
712 assert!(!view.contains("FULLMODE"), "{view}");
713
714 overlay.set_mode(PerfOverlayMode::Full);
716 let _ = App::update(&mut overlay, &mut term, &frame(1));
717 term.present().expect("present");
718 let view = term.backend().format_view();
719 assert!(view.contains("FULLMODE"), "{view}");
720 }
721
722 #[test]
723 fn area_places_the_overlay_flush_against_the_top_right_corner() {
724 let area =
725 PerfOverlayApp::<CountingApp, DefaultPerfRenderer>::area(Size::new(20, 3), 80, 24);
726 assert_eq!(area, Rect::new(60, 0, 20, 3));
727 }
728
729 #[test]
730 fn area_clamps_to_a_terminal_smaller_than_the_requested_size() {
731 let area =
732 PerfOverlayApp::<CountingApp, DefaultPerfRenderer>::area(Size::new(20, 3), 10, 2);
733 assert_eq!(area, Rect::new(0, 0, 10, 2));
734 }
735
736 #[test]
737 fn toggle_key_can_be_overridden() {
738 let mut term = Terminal::new(Headless::new(40, 5));
739 term.backend_mut().push_event(Event::Key(KeyEvent::new(
740 KeyCode::Char('p'),
741 KeyModifiers::NONE,
742 )));
743 let mut overlay = PerfOverlayApp::new(
744 CountingApp {
745 updates: 0,
746 exit_at: 100,
747 },
748 "headless",
749 )
750 .toggle_key(|event| {
751 matches!(
752 event,
753 Event::Key(k) if k.code == KeyCode::Char('p') && k.kind == KeyEventKind::Press
754 )
755 });
756
757 assert!(overlay.is_visible());
758 let _ = App::update(&mut overlay, &mut term, &frame(0));
759 assert!(
760 !overlay.is_visible(),
761 "the overridden toggle key ('p') should have cycled the overlay off"
762 );
763
764 term.backend_mut().push_event(Event::Key(KeyEvent::new(
766 KeyCode::Char('`'),
767 KeyModifiers::NONE,
768 )));
769 let _ = App::update(&mut overlay, &mut term, &frame(1));
770 assert!(
771 !overlay.is_visible(),
772 "backtick should no longer be the toggle key"
773 );
774 }
775
776 #[test]
777 fn inner_accessors_and_into_inner_round_trip() {
778 let mut overlay = PerfOverlayApp::new(
779 CountingApp {
780 updates: 3,
781 exit_at: 100,
782 },
783 "headless",
784 );
785 assert_eq!(overlay.inner().updates, 3);
786 overlay.inner_mut().updates = 7;
787 assert_eq!(overlay.inner().updates, 7);
788 assert_eq!(overlay.into_inner().updates, 7);
789 }
790}