Skip to main content

retroglyph_core/
app.rs

1//! The `App`-driven game loop.
2//!
3//! Where [`Backend`](crate::backend::Backend) is the output contract, [`App`](crate::app::App) is the per-frame update
4//! contract. A game implements [`App`](crate::app::App) once and runs on every backend unchanged.
5//!
6//! The loop decomposes into three pieces:
7//!
8//! - the contract ([`App`](crate::app::App), [`Flow`](crate::app::Flow), [`Frame`](crate::app::Frame)), here in the core;
9//! - the generic blocking driver ([`run_blocking`](crate::app::run_blocking)/[`run_blocking_with`](crate::app::run_blocking_with), `std` only), which
10//!   covers `Crossterm` (in `retroglyph-crossterm`) and [`Headless`](crate::backend::Headless);
11//! - the inverted driver in the windowing layer (the software backend's
12//!   `run_app`), which cannot be generic because winit owns the loop instead of
13//!   handing control back to a shared driver function.
14//!
15//! ```text
16//!                        +-----------------------------+
17//!                        |  App, Flow, Frame (core)    |
18//!                        +-----------------------------+
19//!                                     |
20//!                                App::update
21//!                                     |
22//!               +---------------------+---------------------+
23//!               |                                           |
24//!   run_blocking / run_blocking_with              windowing layer's run_app
25//!   (std only; owns the loop)                     (winit owns the loop instead)
26//!               |                                           |
27//!      crossterm, headless                           software backend
28//! ```
29//!
30//! Both drivers call [`App::update`](crate::app::App::update) as the per-frame body and present automatically after it
31//! returns, skipping the present on [`Flow::Idle`](crate::app::Flow::Idle) or when `update` already presented itself. The
32//! low-level [`poll`](crate::terminal::Terminal::poll) / [`present`](crate::terminal::Terminal::present) API remains
33//! available for turn-based games and headless tests.
34
35use crate::backend::Backend;
36use crate::terminal::Terminal;
37use core::time::Duration;
38
39/// Whether the game loop should continue or stop after a frame, and whether that frame renders.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41#[non_exhaustive]
42pub enum Flow {
43    /// Run another frame, and present it.
44    Continue,
45    /// Run another frame, but nothing changed: skip [`present`](crate::terminal::Terminal::present) and leave the
46    /// previous frame on screen.
47    ///
48    /// For turn-based apps that only need to redraw in response to player input, not on every
49    /// tick of the driver's loop. Returning `Idle` while a `retroglyph_ui::Tween`- or
50    /// [`FrameClock`](crate::frames::FrameClock)-driven animation is still in flight is an
51    /// app bug, not a valid use: an in-progress animation has something new to show every frame,
52    /// which is exactly what `Idle` tells the driver isn't true.
53    Idle,
54    /// Stop the loop. The driver returns and the terminal unwinds normally, so
55    /// backend `Drop` logic (for example crossterm's terminal restore) runs.
56    Exit,
57}
58
59/// Per-frame context handed to [`App::update`](crate::app::App::update).
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Frame {
62    /// Wall-clock time elapsed since the previous frame, supplied by the driver.
63    pub delta: Duration,
64    /// Monotonic frame counter, starting at 0.
65    pub frame: u64,
66}
67
68/// The per-frame update contract for a game.
69///
70/// Implement this once, generically over the backend, to run everywhere:
71///
72/// ```
73/// use retroglyph_core::app::{App, Flow, Frame};
74/// use retroglyph_core::backend::Backend;
75/// use retroglyph_core::color::Style;
76/// use retroglyph_core::terminal::Terminal;
77///
78/// struct MyGame;
79/// impl<B: Backend> App<B> for MyGame {
80///     fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
81///         term.surface().put((0, 0), '@', Style::default());
82///         Flow::Exit
83///     }
84/// }
85/// ```
86pub trait App<B: Backend> {
87    /// Advance and render one frame.
88    ///
89    /// Draw into `term`, read input via `term`, and return [`Flow::Exit`](crate::app::Flow::Exit) to stop the loop.
90    ///
91    /// Draw via [`term.surface()`](crate::terminal::Terminal::surface) or [`term.draw()`](crate::terminal::Terminal::draw) (though
92    /// `draw` presents itself, which usually conflicts with the driver's own automatic present
93    /// below; prefer `surface()` inside `update`). Every driver ([`run_blocking`](crate::app::run_blocking) and
94    /// `retroglyph-window`'s windowed drivers) presents the frame automatically right after this
95    /// method returns, unless it returned [`Flow::Idle`](crate::app::Flow::Idle), in which case the driver skips
96    /// [`present`](crate::terminal::Terminal::present) entirely. Calling `present` yourself inside `update` remains
97    /// fine (the driver detects it already ran via [`present_count`](crate::terminal::Terminal::present_count) and
98    /// skips its own call) but is never required. [`run_blocking`](crate::app::run_blocking) and [`run_blocking_with`](crate::app::run_blocking_with) link
99    /// back here rather than restating this contract.
100    fn update(&mut self, term: &mut Terminal<B>, frame: &Frame) -> Flow;
101}
102
103/// Drive an [`App`](crate::app::App) with a blocking, event-driven loop until it returns [`Flow::Exit`](crate::app::Flow::Exit).
104///
105/// Generic over the backend, so it powers every non-inverted backend
106/// (`Crossterm` in `retroglyph-crossterm`, [`Headless`](crate::backend::Headless))
107/// with no per-backend loop code.
108/// Inverted backends (software/winit) provide their own driver.
109///
110/// The terminal is owned and dropped when the loop exits, so backend teardown
111/// (for example crossterm's terminal restore) runs on the way out.
112///
113/// See [`App::update`](crate::app::App::update) for the present/idle contract this and every other driver follows.
114/// Equivalent to `run_blocking_with(term, app, RunOptions::default())`: on [`Flow::Idle`](crate::app::Flow::Idle), blocks
115/// on input rather than calling `update` again immediately, so a turn-based app that's idle most
116/// of the time costs approximately nothing. Use [`run_blocking_with`](crate::app::run_blocking_with) with [`RunOptions::animated`](crate::app::RunOptions::animated)
117/// for a continuously-rendering app instead.
118///
119/// # Errors
120///
121/// Returns the backend's error if the automatic `present()` call fails. The loop stops and the
122/// terminal is dropped (running backend teardown) before the error is returned.
123#[cfg(feature = "std")]
124pub fn run_blocking<B, A>(term: Terminal<B>, app: A) -> Result<(), B::Error>
125where
126    B: Backend,
127    A: App<B>,
128{
129    run_blocking_with(term, app, RunOptions::default())
130}
131
132/// Options controlling [`run_blocking_with`](crate::app::run_blocking_with)'s pacing and idle behavior.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134#[non_exhaustive]
135pub struct RunOptions {
136    target_fps: Option<u32>,
137    event_driven: bool,
138    idle_wake: Option<Duration>,
139}
140
141impl RunOptions {
142    /// Options for a continuously-rendering, [`target_fps`](Self::target_fps)-paced loop.
143    ///
144    /// [`event_driven`](Self::event_driven) is `false`: [`Flow::Idle`](crate::app::Flow::Idle) only skips `present`, it
145    /// never blocks. Use this for apps that drive a `retroglyph_ui::Tween`/
146    /// [`FrameClock`](crate::frames::FrameClock) from [`Frame::delta`](crate::app::Frame::delta) and need `update`
147    /// called every tick regardless of input.
148    ///
149    /// `target_fps` becomes [`RunOptions::target_fps`](crate::app::RunOptions::target_fps) verbatim, including `0`: passing `0` here
150    /// builds without panicking, but [`run_blocking_with`](crate::app::run_blocking_with) panics once it constructs the
151    /// [`FrameClock`](crate::frames::FrameClock) that paces it (see that function's
152    /// `# Panics` section).
153    #[must_use]
154    pub const fn animated(target_fps: u32) -> Self {
155        Self {
156            target_fps: Some(target_fps),
157            event_driven: false,
158            idle_wake: None,
159        }
160    }
161
162    /// Caps the loop at this many [`App::update`](crate::app::App::update) calls per second whenever a frame actually
163    /// runs, using a [`FrameClock`](crate::frames::FrameClock) internally to pace them
164    /// evenly. `None` (the default) runs uncapped: as fast as `update` allows for back-to-back
165    /// [`Flow::Continue`](crate::app::Flow::Continue) frames, or immediately after whatever woke an
166    /// [`event_driven`](Self::event_driven) loop from [`Flow::Idle`](crate::app::Flow::Idle).
167    #[must_use]
168    pub const fn with_target_fps(mut self, target_fps: u32) -> Self {
169        self.target_fps = Some(target_fps);
170        self
171    }
172
173    /// Returns the configured [`target_fps`](Self::with_target_fps) cap, if any.
174    #[must_use]
175    pub const fn target_fps(&self) -> Option<u32> {
176        self.target_fps
177    }
178
179    /// On [`Flow::Idle`](crate::app::Flow::Idle), block on input instead of calling `update` again immediately.
180    ///
181    /// `true` (the default) is right for turn-based, event-driven apps that are idle most of the
182    /// time: an idle frame costs approximately nothing, blocked in the backend's input read
183    /// rather than spinning `update` as fast as the host can manage. `false` keeps `Flow::Idle`
184    /// non-blocking (skip `present`, keep looping at whatever rate
185    /// [`target_fps`](Self::target_fps) allows): right for apps that animate from
186    /// [`Frame::delta`](crate::app::Frame::delta) and only return `Idle` between animation-driven `Continue` frames, where
187    /// blocking would freeze the animation until the next stray input event. See
188    /// [`RunOptions::animated`](crate::app::RunOptions::animated) for that shape.
189    #[must_use]
190    pub const fn event_driven(mut self, event_driven: bool) -> Self {
191        self.event_driven = event_driven;
192        self
193    }
194
195    /// Returns whether [`Flow::Idle`](crate::app::Flow::Idle) blocks on input rather than looping immediately.
196    #[must_use]
197    pub const fn is_event_driven(&self) -> bool {
198        self.event_driven
199    }
200
201    /// When [`is_event_driven`](Self::is_event_driven) is `true`, the longest an idle loop blocks
202    /// before calling `update` again anyway, even with no input. `None` (the default) blocks
203    /// indefinitely: right for apps with nothing to redraw until input arrives. `Some(d)`
204    /// additionally wakes the loop every `d`, for apps that need a periodic idle redraw (a
205    /// blinking cursor, a clock) without paying full frame-rate cost. Ignored when
206    /// [`is_event_driven`](Self::is_event_driven) is `false`.
207    #[must_use]
208    pub const fn with_idle_wake(mut self, idle_wake: Duration) -> Self {
209        self.idle_wake = Some(idle_wake);
210        self
211    }
212
213    /// Returns the configured [`idle_wake`](Self::with_idle_wake) interval, if any.
214    #[must_use]
215    pub const fn idle_wake(&self) -> Option<Duration> {
216        self.idle_wake
217    }
218}
219
220impl Default for RunOptions {
221    /// Event-driven, uncapped, blocks indefinitely on [`Flow::Idle`](crate::app::Flow::Idle): see [`run_blocking`](crate::app::run_blocking).
222    fn default() -> Self {
223        Self {
224            target_fps: None,
225            event_driven: true,
226            idle_wake: None,
227        }
228    }
229}
230
231/// Drive an [`App`](crate::app::App) with a blocking loop until it returns [`Flow::Exit`](crate::app::Flow::Exit), paced by `options`.
232///
233/// The zero-config [`run_blocking`](crate::app::run_blocking) is equivalent to `run_blocking_with(term, app,
234/// RunOptions::default())`. Pass [`RunOptions::animated`](crate::app::RunOptions::animated) for a continuously-rendering loop
235/// capped at a fixed rate instead, using a [`FrameClock`](crate::frames::FrameClock)
236/// internally so `update` is called at even intervals rather than however fast the host can
237/// spin.
238///
239/// With [`RunOptions::is_event_driven`](crate::app::RunOptions::is_event_driven) `true` (the default), [`Flow::Idle`](crate::app::Flow::Idle) blocks the loop on
240/// input (via [`Terminal::wait_for_input`](crate::terminal::Terminal::wait_for_input)) instead of calling `update` again immediately:
241/// an idle app has nothing new to show, so there is no reason to burn CPU polling it at all,
242/// let alone faster than any configured rate. With `event_driven` `false`, an idle loop still
243/// waits out the remainder of the current `target_fps` interval (if set) before calling `update`
244/// again, rather than looping immediately, but never blocks on input.
245///
246/// # Errors
247///
248/// Returns the backend's error if the automatic `present()` call fails. The loop stops and the
249/// terminal is dropped (running backend teardown) before the error is returned.
250///
251/// # Panics
252///
253/// Panics if `options.target_fps` is `Some(0)`: pacing at a `FrameClock` internally, which
254/// requires a non-zero rate (see [`FrameClock::new`](crate::frames::FrameClock::new)).
255#[cfg(feature = "std")]
256pub fn run_blocking_with<B, A>(
257    mut term: Terminal<B>,
258    mut app: A,
259    options: RunOptions,
260) -> Result<(), B::Error>
261where
262    B: Backend,
263    A: App<B>,
264{
265    let mut clock = options.target_fps().map(crate::frames::FrameClock::new);
266    let mut frame_count = 0u64;
267    let mut last = std::time::Instant::now();
268    loop {
269        if let Some(clock) = clock.as_mut() {
270            // Block out the rest of this frame's budget before ticking `update` again, so a
271            // paced loop doesn't busy-spin between updates the way an uncapped one does.
272            let elapsed = last.elapsed();
273            if let Some(remaining) = clock.step().checked_sub(elapsed) {
274                std::thread::sleep(remaining);
275            }
276            clock.advance(clock.step().max(elapsed));
277            // A fixed-timestep `FrameClock` is meant to be drained in a `while tick()` loop for
278            // logic that must run in whole steps; here it only paces wall-clock timing, so a
279            // single `tick()` (there is always at least one step ready, since we just slept/
280            // advanced past the threshold) resets the accumulator for the next iteration.
281            let _ = clock.tick();
282        }
283        let now = std::time::Instant::now();
284        let delta = now.duration_since(last);
285        last = now;
286        let frame = Frame {
287            delta,
288            frame: frame_count,
289        };
290        frame_count = frame_count.wrapping_add(1);
291        let present_count_before = term.present_count();
292        let flow = app.update(&mut term, &frame);
293        if flow == Flow::Exit {
294            return Ok(());
295        }
296        // A no-op if `update` already called `present()` itself (detected via `present_count`
297        // rather than relying on `present()` being a safe no-op to call twice: it always presents
298        // unconditionally, so a second call here would diff the just-cleared `current` against
299        // the just-presented `previous` and erase the frame `update` already sent).
300        if flow != Flow::Idle && term.present_count() == present_count_before {
301            term.present()?;
302        }
303        // `Flow` is `#[non_exhaustive]`; treat any variant other than `Exit`/`Idle` the same as
304        // `Continue` (keep looping and presenting) rather than exiting on an unknown future value.
305        if flow == Flow::Idle && options.is_event_driven() {
306            // The heart of the fix for retroglyph#603: block here instead of immediately
307            // re-entering the loop, so an idle frame costs approximately nothing rather than
308            // spinning `update` as fast as the host allows. `wait_for_input` buffers any event it
309            // finds rather than consuming it, so the app's own `update` still observes it on the
310            // next iteration; this call only answers "did something happen", it doesn't steal
311            // the event. A `target_fps` clock (if set) still gets its top-of-loop sleep on the
312            // next iteration; it isn't bypassed by waking early.
313            term.wait_for_input(options.idle_wake().unwrap_or(Duration::MAX));
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::backend::Headless;
322    use crate::event::{Event, KeyCode, KeyEvent, KeyModifiers};
323
324    struct Counter {
325        frames: u64,
326    }
327
328    impl App<Headless> for Counter {
329        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
330            self.frames += 1;
331            term.surface()
332                .put((0, 0), '#', crate::color::Style::default());
333            term.present().expect("present");
334            // Quit when a key is pending, or after a safety cap.
335            if term.has_input() || frame.frame >= 100 {
336                Flow::Exit
337            } else {
338                Flow::Continue
339            }
340        }
341    }
342
343    #[cfg(feature = "std")]
344    #[test]
345    fn run_blocking_exits_on_flow_exit() {
346        let mut backend = Headless::new(4, 1);
347        backend.push_event(Event::Key(KeyEvent::new(
348            KeyCode::Char('q'),
349            KeyModifiers::NONE,
350        )));
351        let term = Terminal::new(backend);
352        let app = Counter { frames: 0 };
353        // Runs until the queued key is observed. Reaching the next line proves
354        // the loop terminated on Flow::Exit rather than spinning forever.
355        run_blocking(term, app).expect("run_blocking");
356    }
357
358    /// An app that never draws and always returns `Idle` except on the last frame: proves
359    /// `run_blocking` skips `present()` for `Idle` frames rather than erasing an untouched grid.
360    struct AlwaysIdle {
361        frames: u64,
362    }
363
364    impl App<Headless> for AlwaysIdle {
365        fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
366            self.frames += 1;
367            if frame.frame >= 5 {
368                Flow::Exit
369            } else {
370                Flow::Idle
371            }
372        }
373    }
374
375    #[cfg(feature = "std")]
376    #[test]
377    fn run_blocking_skips_present_on_idle() {
378        let term = Terminal::new(Headless::new(2, 1));
379        let app = AlwaysIdle { frames: 0 };
380        // `update` never draws or presents; if the driver called `present()` on an `Idle` frame
381        // anyway it would be harmless here (nothing to erase), so this mainly documents intent --
382        // the presenting behavior itself is covered by `run_blocking_with_options_presents_frames`.
383        run_blocking(term, app).expect("run_blocking");
384    }
385
386    /// An app that draws a distinct glyph per frame and never presents itself, so successfully
387    /// reaching the backend proves the driver's automatic present ran.
388    struct DrawsAndExits {
389        frames: u64,
390        exit_at: u64,
391    }
392
393    impl App<Headless> for DrawsAndExits {
394        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
395            self.frames += 1;
396            term.surface()
397                .put((0, 0), 'x', crate::color::Style::default());
398            if frame.frame >= self.exit_at {
399                Flow::Exit
400            } else {
401                Flow::Continue
402            }
403        }
404    }
405
406    #[cfg(feature = "std")]
407    #[test]
408    fn run_blocking_presents_automatically() {
409        let term = Terminal::new(Headless::new(2, 1));
410        let app = DrawsAndExits {
411            frames: 0,
412            exit_at: 0,
413        };
414        run_blocking(term, app).expect("run_blocking");
415        // No assertion on backend content is possible here: `term` is consumed by `run_blocking`.
416        // Coverage that the automatic present actually reaches the backend lives in
417        // `retroglyph-window`'s own driver tests, which retain the terminal after the loop.
418    }
419
420    #[cfg(feature = "std")]
421    #[test]
422    fn run_blocking_with_default_options_matches_run_blocking() {
423        let term = Terminal::new(Headless::new(2, 1));
424        let app = DrawsAndExits {
425            frames: 0,
426            exit_at: 2,
427        };
428        run_blocking_with(term, app, RunOptions::default()).expect("run_blocking_with");
429    }
430
431    #[cfg(feature = "std")]
432    #[test]
433    fn run_blocking_with_animated_options_runs_to_completion() {
434        let term = Terminal::new(Headless::new(2, 1));
435        let app = DrawsAndExits {
436            frames: 0,
437            exit_at: 2,
438        };
439        // A high cap keeps this test fast; the point is that a paced loop still terminates on
440        // `Flow::Exit` and delivers the same number of updates as an uncapped loop would.
441        run_blocking_with(term, app, RunOptions::animated(1000)).expect("run_blocking_with");
442    }
443
444    #[test]
445    fn run_options_animated_sets_fields() {
446        let animated = RunOptions::animated(30);
447        assert_eq!(animated.target_fps(), Some(30));
448        assert!(!animated.is_event_driven());
449        assert_eq!(animated.idle_wake(), None);
450
451        let default = RunOptions::default();
452        assert_eq!(default.target_fps(), None);
453        assert!(default.is_event_driven());
454        assert_eq!(default.idle_wake(), None);
455    }
456
457    #[test]
458    fn run_options_setters_override_defaults() {
459        let options = RunOptions::default()
460            .with_target_fps(60)
461            .event_driven(false)
462            .with_idle_wake(Duration::from_millis(250));
463        assert_eq!(options.target_fps(), Some(60));
464        assert!(!options.is_event_driven());
465        assert_eq!(options.idle_wake(), Some(Duration::from_millis(250)));
466    }
467
468    /// An app that returns `Idle` for its first frame, then `Exit`. The queued key is only
469    /// pushed into the backend *after* the driver would have already woken from the idle wait
470    /// (`Headless::poll_event` ignores its timeout and returns immediately either way), so this
471    /// mainly documents the contract at the type level: `event_driven: false` is accepted and the
472    /// loop still terminates, i.e. the non-blocking `Idle` shape is a supported option for
473    /// animated apps. Real blocking behavior (`event_driven: true` actually parking
474    /// the thread) can only be observed on a backend that genuinely blocks, like crossterm --
475    /// see that crate's own tests.
476    struct IdleThenExit {
477        frames: u64,
478    }
479
480    impl App<Headless> for IdleThenExit {
481        fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
482            self.frames += 1;
483            if frame.frame == 0 {
484                Flow::Idle
485            } else {
486                Flow::Exit
487            }
488        }
489    }
490
491    #[cfg(feature = "std")]
492    #[test]
493    fn run_blocking_with_non_event_driven_options_does_not_block_on_idle() {
494        let term = Terminal::new(Headless::new(2, 1));
495        let app = IdleThenExit { frames: 0 };
496        let options = RunOptions {
497            target_fps: None,
498            event_driven: false,
499            idle_wake: None,
500        };
501        run_blocking_with(term, app, options).expect("run_blocking_with");
502    }
503
504    /// Proves the driver's idle wait doesn't swallow the event it woke up for: `update` is only
505    /// ever called again *after* `wait_for_input` observed something, so the app's own `has_input`
506    /// must still see the same event on the next frame rather than the driver having consumed it.
507    struct ObservesQueuedEventAfterIdle {
508        frames: u64,
509        saw_input_after_idle: bool,
510    }
511
512    impl App<Headless> for ObservesQueuedEventAfterIdle {
513        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
514            self.frames += 1;
515            if frame.frame == 0 {
516                return Flow::Idle;
517            }
518            self.saw_input_after_idle = term.has_input();
519            Flow::Exit
520        }
521    }
522
523    #[test]
524    fn run_blocking_event_driven_idle_wait_does_not_consume_the_waking_event() {
525        let mut backend = Headless::new(2, 1);
526        backend.push_event(Event::Key(KeyEvent::new(
527            KeyCode::Char('x'),
528            KeyModifiers::NONE,
529        )));
530        let term = Terminal::new(backend);
531        let mut app = ObservesQueuedEventAfterIdle {
532            frames: 0,
533            saw_input_after_idle: false,
534        };
535        // Can't recover `app` through `run_blocking` (it takes the app by value and drops it with
536        // the terminal), so drive the loop by hand via `step`, mirroring what `run_blocking_with`
537        // does around the `Flow::Idle` branch.
538        let mut term = term;
539        let frame0 = Frame {
540            delta: Duration::ZERO,
541            frame: 0,
542        };
543        assert_eq!(app.update(&mut term, &frame0), Flow::Idle);
544        // This is the exact call `run_blocking_with` makes on `Flow::Idle` when `event_driven` is
545        // `true`: it must buffer the event, not return/consume it, so `update`'s own `has_input`
546        // still finds it below.
547        assert!(term.wait_for_input(Duration::MAX));
548        let frame1 = Frame {
549            delta: Duration::ZERO,
550            frame: 1,
551        };
552        assert_eq!(app.update(&mut term, &frame1), Flow::Exit);
553        assert!(app.saw_input_after_idle);
554    }
555}