Skip to main content

retroglyph_ui/perf/
mod.rs

1//! [`PerfOverlayApp`]: a live frame-time/FPS overlay for any `App`, on any `Backend`.
2//!
3//! Wrap an existing [`App`](retroglyph_core::app::App) once and it gains a toggleable perf readout on
4//! every backend, with no backend-specific code in the wrapped app itself:
5//!
6//! ```
7//! # #[cfg(feature = "std")]
8//! # {
9//! use retroglyph_core::app::run_blocking;
10//! use retroglyph_core::backend::{Backend, Headless};
11//! use retroglyph_core::terminal::Terminal;
12//! # use retroglyph_core::app::{App, Flow, Frame};
13//! use retroglyph_ui::PerfOverlayApp;
14//! # struct MyGame;
15//! # impl<B: Backend> App<B> for MyGame {
16//! #     fn update(&mut self, _term: &mut Terminal<B>, frame: &Frame) -> Flow {
17//! #         if frame.frame >= 3 { Flow::Exit } else { Flow::Continue }
18//! #     }
19//! # }
20//!
21//! let term = Terminal::new(Headless::new(40, 10));
22//! let app = PerfOverlayApp::new(MyGame, "headless");
23//! run_blocking(term, app).expect("run_blocking");
24//! # } // `run_blocking` is `std`-only; a no-op under `--no-default-features`.
25//! ```
26//!
27//! # What's generic and what's backend-specific
28//!
29//! The frame-time bookkeeping ([`FrameStats`](retroglyph_core::frames::FrameStats)), the toggle-key check,
30//! and the decision of when to draw are all backend-agnostic:
31//! [`PerfOverlayApp::update`](retroglyph_core::app::App::update) only ever talks to
32//! [`Terminal`](retroglyph_core::terminal::Terminal)/[`Surface`](crate::Surface), which every
33//! [`Backend`](retroglyph_core::backend::Backend) implements identically. The one thing every caller still
34//! supplies by hand is the `backend` label string (there is no portable way to ask a `Backend`
35//! what to call itself); everything else, including toggling the overlay on and off, works
36//! unmodified on crossterm, a native window, or a browser tab.
37//!
38//! # Rendering
39//!
40//! [`DefaultPerfRenderer`] (used by [`PerfOverlayApp::new`]) draws a single-row `NNNfps MM.Mms
41//! minMM.M maxMM.M <backend>` readout, compact enough to fit an 80-column terminal alongside a
42//! long backend label. For a richer overlay (a bordered panel, a frame-time sparkline, extra
43//! app-supplied metrics like resolution or vsync state), pass a closure to
44//! [`PerfOverlayApp::with_closure`]: composing this crate's [`Panel`](crate::Panel)/
45//! [`Sparkline`](crate::Sparkline) widgets, or [`PerfOverlay`](crate::PerfOverlay) directly,
46//! requires no glue code beyond the closure itself. Implement [`PerfRenderer`] directly, and
47//! construct with [`PerfOverlayApp::with_renderer`], only for a named, reusable renderer type
48//! instead of a closure.
49//!
50//! This whole wrapper exists in large part because
51//! [`FrameStats::record`](retroglyph_core::frames::FrameStats::record) needs a
52//! [`Frame`](retroglyph_core::app::Frame), which a plain widget draw call had no way to reach:
53//! [`PerfOverlayApp::update`](retroglyph_core::app::App::update) is what intercepts `Frame` on the way
54//! through and calls `FrameStats::record` for the widget that otherwise couldn't. An app that
55//! doesn't need this wrapper's other job (generic toggle-key handling across any wrapped
56//! [`App`](retroglyph_core::app::App), on every backend) no longer needs it just for that:
57//! [`AnimatedPerfOverlay`](crate::AnimatedPerfOverlay) reaches `Frame` directly, so an app that
58//! already owns a `FrameStats` field can record and draw it in a single call, with no decorator at
59//! all.
60//!
61//! # Toggling
62//!
63//! [`PerfOverlayApp::update`](retroglyph_core::app::App::update) drains every event out of the wrapped
64//! [`Terminal`](retroglyph_core::terminal::Terminal) before handing control to the inner
65//! [`App`](retroglyph_core::app::App), keeps any that match the toggle key (backtick, or F1 as an
66//! alias, by default; see [`default_is_toggle_key`]), and re-queues the rest via
67//! [`Terminal::requeue_events`](retroglyph_core::terminal::Terminal::requeue_events) so the inner app sees
68//! exactly the input it would have without the overlay, minus the toggle presses. This works
69//! identically on every backend because it only goes through
70//! [`Terminal`](retroglyph_core::terminal::Terminal)'s own event queue, never a backend-specific input path
71//! (in particular, never
72//! [`Input::push_event`](retroglyph_core::backend::Input::push_event), whose documented default is
73//! a no-op for backends that never receive events from outside their own `poll_event`).
74//!
75//! The toggle key doesn't just flip visibility: it cycles through [`PerfOverlayMode`]:
76//! [`Off`](PerfOverlayMode::Off) -> [`Compact`](PerfOverlayMode::Compact) ->
77//! [`Full`](PerfOverlayMode::Full) -> back to `Off`. `Full` only exists once
78//! [`PerfOverlayApp::cycle_with`] registers a second, richer [`PerfRenderer`] (typically
79//! [`PerfOverlay`](crate::PerfOverlay), a bordered panel with a frame-time
80//! [`Sparkline`](crate::Sparkline)); without it, the cycle degrades to the plain two-state
81//! `Off`/`Compact` toggle.
82
83mod app;
84mod mode;
85mod renderer;
86
87pub use app::{PerfOverlayApp, default_is_toggle_key};
88pub use mode::PerfOverlayMode;
89pub use renderer::{DefaultPerfRenderer, PerfRenderer};
90
91use retroglyph_core::surface::Layer;
92
93/// How many frames [`PerfOverlayApp`]'s internal [`FrameStats`](retroglyph_core::frames::FrameStats)
94/// remembers.
95///
96/// About two seconds at 60fps. Not configurable per instance: pick a bigger window by building a
97/// `FrameStats` directly and rendering it through a custom [`PerfRenderer`] closure instead of
98/// [`PerfOverlayApp`], if a specific app genuinely needs one.
99pub const FRAME_HISTORY: usize = 120;
100
101/// [`PerfOverlayApp`]'s default overlay layer: [`Layer::Debug`].
102///
103/// The workspace's named top-most UI tier, so a perf HUD stays visible over whatever else is on
104/// screen (including an open [`Layer::Overlay`] popup) rather than risking a lower, app-chosen
105/// layer hiding it. Override with [`PerfOverlayApp::layer`] if an app's own content already
106/// reaches this layer.
107pub const DEFAULT_LAYER: u8 = Layer::Debug.as_u8();