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`](crate::app::run)/[`run_with`](crate::app::run_with), and the
10//!   `Terminal`-taking [`run_on`](crate::app::run_on)/[`run_on_with`](crate::app::run_on_with)
11//!   they're built on, `std` only), which covers `Crossterm` (in `retroglyph-crossterm`) and
12//!   [`Headless`](crate::backend::Headless);
13//! - the inverted driver in the windowing layer (the software backend's
14//!   `run_app`), which cannot be generic because winit owns the loop instead of
15//!   handing control back to a shared driver function.
16//!
17//! ```text
18//!                        +-----------------------------+
19//!                        |  App, Flow, Frame (core)    |
20//!                        +-----------------------------+
21//!                                     |
22//!                                App::update
23//!                                     |
24//!               +---------------------+---------------------+
25//!               |                                           |
26//!   run_on / run_on_with              windowing layer's run_app
27//!   (std only; owns the loop)                     (winit owns the loop instead)
28//!               |                                           |
29//!      crossterm, headless                           software backend
30//! ```
31//!
32//! Both drivers call [`App::update`](crate::app::App::update) as the per-frame body and present automatically after it
33//! returns, skipping the present on [`Flow::Idle`](crate::app::Flow::Idle) or when `update` already presented itself. The
34//! low-level [`poll`](crate::terminal::Terminal::poll) / [`present`](crate::terminal::Terminal::present) API remains
35//! available for turn-based games and headless tests.
36
37use crate::backend::Backend;
38use crate::terminal::Terminal;
39use core::time::Duration;
40
41/// Whether the game loop should continue or stop after a frame, and whether that frame renders.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum Flow {
45    /// Run another frame, and present it.
46    Continue,
47    /// Run another frame, but nothing changed: skip [`present`](crate::terminal::Terminal::present) and leave the
48    /// previous frame on screen.
49    ///
50    /// For turn-based apps that only need to redraw in response to player input, not on every
51    /// tick of the driver's loop. Returning `Idle` while a `retroglyph_ui::animate::Tween`- or
52    /// [`FrameClock`](crate::frames::FrameClock)-driven animation is still in flight is an
53    /// app bug, not a valid use: an in-progress animation has something new to show every frame,
54    /// which is exactly what `Idle` tells the driver isn't true.
55    Idle,
56    /// Stop the loop. The driver returns and the terminal unwinds normally, so
57    /// backend `Drop` logic (for example crossterm's terminal restore) runs.
58    Exit,
59}
60
61/// Per-frame context handed to [`App::update`](crate::app::App::update).
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[non_exhaustive]
64pub struct Frame {
65    /// Wall-clock time elapsed since the previous frame, supplied by the driver.
66    pub delta: Duration,
67    /// Monotonic frame counter, starting at 0.
68    pub frame: u64,
69}
70
71impl Frame {
72    /// A frame at `frame` with `delta` elapsed since the previous one.
73    #[must_use]
74    pub const fn new(delta: Duration, frame: u64) -> Self {
75        Self { delta, frame }
76    }
77}
78
79/// The per-frame update contract for a game.
80///
81/// Implement this once, generically over the backend, to run everywhere:
82///
83/// ```
84/// use retroglyph_core::app::{App, Flow, Frame};
85/// use retroglyph_core::backend::Backend;
86/// use retroglyph_core::color::Style;
87/// use retroglyph_core::terminal::Terminal;
88///
89/// struct MyGame;
90/// impl<B: Backend> App<B> for MyGame {
91///     fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
92///         term.surface().put((0, 0), '@', Style::default());
93///         Flow::Exit
94///     }
95/// }
96/// ```
97pub trait App<B: Backend> {
98    /// Runs once, before the first [`update`](Self::update) call, with the first live `term` for
99    /// whichever backend is actually running.
100    ///
101    /// The hook for anything whose initial state depends on the real starting grid size
102    /// (`term.backend().size()`), which varies by backend: crossterm reports the actual terminal
103    /// size, a windowed backend reports whatever grid it was configured with. Every driver in
104    /// this crate ([`run_on_with`](crate::app::run_on_with), [`TestHarness::step`](crate::testing::TestHarness::step),
105    /// `retroglyph-recorder`'s `replay_live`) and `retroglyph-window`'s windowed driver call this
106    /// exactly once, before the first `update`.
107    ///
108    /// Default: does nothing. An app with no backend-dependent startup state never needs to
109    /// override this; one that does can build its state here instead of deferring construction
110    /// behind an `Option<State>` field lazily initialized on the first `update` call.
111    fn init(&mut self, _term: &mut Terminal<B>) {}
112
113    /// Advance and render one frame.
114    ///
115    /// Draw into `term`, read input via `term`, and return [`Flow::Exit`](crate::app::Flow::Exit) to stop the loop.
116    ///
117    /// Draw via [`term.surface()`](crate::terminal::Terminal::surface) or [`term.draw()`](crate::terminal::Terminal::draw) (though
118    /// `draw` presents itself, which usually conflicts with the driver's own automatic present
119    /// below; prefer `surface()` inside `update`). Every driver ([`run_on`](crate::app::run_on) and
120    /// `retroglyph-window`'s windowed drivers) presents the frame automatically right after this
121    /// method returns, unless it returned [`Flow::Idle`](crate::app::Flow::Idle), in which case the driver skips
122    /// [`present`](crate::terminal::Terminal::present) entirely. Calling `present` yourself inside `update` remains
123    /// fine (the driver detects it already ran via [`present_count`](crate::terminal::Terminal::present_count) and
124    /// skips its own call) but is never required. [`run_on`](crate::app::run_on) and [`run_on_with`](crate::app::run_on_with) link
125    /// back here rather than restating this contract.
126    fn update(&mut self, term: &mut Terminal<B>, frame: &Frame) -> Flow;
127}
128
129/// Drive an [`App`](crate::app::App) with a blocking, event-driven loop until it returns [`Flow::Exit`](crate::app::Flow::Exit).
130///
131/// Generic over the backend, so it powers every non-inverted backend
132/// (`Crossterm` in `retroglyph-crossterm`, [`Headless`](crate::backend::Headless))
133/// with no per-backend loop code.
134/// Inverted backends (software/winit) provide their own driver.
135///
136/// The terminal is owned and dropped when the loop exits, so backend teardown
137/// (for example crossterm's terminal restore) runs on the way out.
138///
139/// See [`App::update`](crate::app::App::update) for the present/idle contract this and every other driver follows.
140/// Equivalent to `run_on_with(term, app, RunOptions::default())`: on [`Flow::Idle`](crate::app::Flow::Idle), blocks
141/// on input rather than calling `update` again immediately, so a turn-based app that's idle most
142/// of the time costs approximately nothing. Use [`run_on_with`](crate::app::run_on_with) with [`RunOptions::animated`](crate::app::RunOptions::animated)
143/// for a continuously-rendering app instead.
144///
145/// # Errors
146///
147/// Returns the backend's error if the automatic `present()` call fails. The loop stops and the
148/// terminal is dropped (running backend teardown) before the error is returned.
149#[cfg(feature = "std")]
150pub fn run_on<B, A>(term: Terminal<B>, app: A) -> Result<(), B::Error>
151where
152    B: Backend,
153    A: App<B>,
154{
155    run_on_with(term, app, RunOptions::default())
156}
157
158/// Whether [`Flow::Idle`](crate::app::Flow::Idle) blocks the loop on input or keeps looping without presenting.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum Idle {
162    /// Block on input (via [`Terminal::wait_for_input`](crate::terminal::Terminal::wait_for_input))
163    /// instead of calling `update` again immediately. `wake` is the longest an idle loop blocks
164    /// before calling `update` again anyway, even with no input: `None` blocks indefinitely,
165    /// right for apps with nothing to redraw until input arrives; `Some(d)` additionally wakes
166    /// the loop every `d`, for apps that need a periodic idle redraw (a blinking cursor, a
167    /// clock) without paying full frame-rate cost.
168    Block {
169        /// The longest an idle loop blocks before calling `update` again, or `None` to block
170        /// indefinitely.
171        wake: Option<Duration>,
172    },
173    /// Keep `Flow::Idle` non-blocking: skip `present`, keep looping at whatever rate
174    /// [`RunOptions::target_fps`](crate::app::RunOptions::target_fps) allows. Right for apps that animate from
175    /// [`Frame::delta`](crate::app::Frame::delta) and only return `Idle` between animation-driven
176    /// `Continue` frames, where blocking would freeze the animation until the next stray input
177    /// event. See [`RunOptions::animated`](crate::app::RunOptions::animated) for that shape.
178    Spin,
179}
180
181/// Options controlling [`run_on_with`](crate::app::run_on_with)'s pacing and idle behavior.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183#[non_exhaustive]
184pub struct RunOptions {
185    target_fps: Option<u32>,
186    idle: Idle,
187}
188
189impl RunOptions {
190    /// Options for a continuously-rendering, [`target_fps`](Self::target_fps)-paced loop.
191    ///
192    /// [`event_driven`](Self::event_driven) is `false`: [`Flow::Idle`](crate::app::Flow::Idle) only skips `present`, it
193    /// never blocks. Use this for apps that drive a `retroglyph_ui::animate::Tween`/
194    /// [`FrameClock`](crate::frames::FrameClock) from [`Frame::delta`](crate::app::Frame::delta) and need `update`
195    /// called every tick regardless of input.
196    ///
197    /// `target_fps` becomes [`RunOptions::target_fps`](crate::app::RunOptions::target_fps) verbatim, including `0`:
198    /// [`run_on_with`](crate::app::run_on_with) treats `Some(0)` as uncapped, the same as `None`,
199    /// rather than deriving a zero-length frame budget from it.
200    #[must_use]
201    pub const fn animated(target_fps: u32) -> Self {
202        Self {
203            target_fps: Some(target_fps),
204            idle: Idle::Spin,
205        }
206    }
207
208    /// Caps the loop at this many [`App::update`](crate::app::App::update) calls per second whenever a frame actually
209    /// runs, sleeping out the remainder of each frame's budget to pace them
210    /// evenly. `None` (the default) runs uncapped: as fast as `update` allows for back-to-back
211    /// [`Flow::Continue`](crate::app::Flow::Continue) frames, or immediately after whatever woke an
212    /// [`event_driven`](Self::event_driven) loop from [`Flow::Idle`](crate::app::Flow::Idle).
213    #[must_use]
214    pub const fn with_target_fps(mut self, target_fps: u32) -> Self {
215        self.target_fps = Some(target_fps);
216        self
217    }
218
219    /// Returns the configured [`target_fps`](Self::with_target_fps) cap, if any.
220    #[must_use]
221    pub const fn target_fps(&self) -> Option<u32> {
222        self.target_fps
223    }
224
225    /// On [`Flow::Idle`](crate::app::Flow::Idle), block on input instead of calling `update` again immediately.
226    ///
227    /// `true` (the default) is right for turn-based, event-driven apps that are idle most of the
228    /// time: an idle frame costs approximately nothing, blocked in the backend's input read
229    /// rather than spinning `update` as fast as the host can manage. `false` keeps `Flow::Idle`
230    /// non-blocking (skip `present`, keep looping at whatever rate
231    /// [`target_fps`](Self::target_fps) allows): right for apps that animate from
232    /// [`Frame::delta`](crate::app::Frame::delta) and only return `Idle` between animation-driven `Continue` frames, where
233    /// blocking would freeze the animation until the next stray input event. See
234    /// [`RunOptions::animated`](crate::app::RunOptions::animated) for that shape.
235    #[must_use]
236    pub const fn event_driven(mut self, event_driven: bool) -> Self {
237        self.idle = if event_driven {
238            Idle::Block { wake: None }
239        } else {
240            Idle::Spin
241        };
242        self
243    }
244
245    /// Returns whether [`Flow::Idle`](crate::app::Flow::Idle) blocks on input rather than looping immediately.
246    #[must_use]
247    pub const fn is_event_driven(&self) -> bool {
248        matches!(self.idle, Idle::Block { .. })
249    }
250
251    /// When [`is_event_driven`](Self::is_event_driven) is `true`, the longest an idle loop blocks
252    /// before calling `update` again anyway, even with no input. `None` (the default) blocks
253    /// indefinitely: right for apps with nothing to redraw until input arrives. `Some(d)`
254    /// additionally wakes the loop every `d`, for apps that need a periodic idle redraw (a
255    /// blinking cursor, a clock) without paying full frame-rate cost. Builder calls apply in
256    /// order: this always switches [`idle`](Self::idle) to [`Idle::Block`] with `idle_wake`,
257    /// even after a preceding [`event_driven(false)`](Self::event_driven) or
258    /// [`animated`](Self::animated); a later `event_driven(false)` still switches back to
259    /// [`Idle::Spin`] and drops the wake interval.
260    #[must_use]
261    pub const fn with_idle_wake(mut self, idle_wake: Duration) -> Self {
262        self.idle = Idle::Block {
263            wake: Some(idle_wake),
264        };
265        self
266    }
267
268    /// Returns the configured [`idle_wake`](Self::with_idle_wake) interval, if any.
269    #[must_use]
270    pub const fn idle_wake(&self) -> Option<Duration> {
271        match self.idle {
272            Idle::Block { wake } => wake,
273            Idle::Spin => None,
274        }
275    }
276
277    /// Returns the configured [`Idle`] behavior.
278    #[must_use]
279    pub const fn idle(&self) -> Idle {
280        self.idle
281    }
282}
283
284impl Default for RunOptions {
285    /// Event-driven, uncapped, blocks indefinitely on [`Flow::Idle`](crate::app::Flow::Idle): see [`run_on`](crate::app::run_on).
286    fn default() -> Self {
287        Self {
288            target_fps: None,
289            idle: Idle::Block { wake: None },
290        }
291    }
292}
293
294/// Drive an [`App`](crate::app::App) with a blocking loop until it returns [`Flow::Exit`](crate::app::Flow::Exit), paced by `options`.
295///
296/// The zero-config [`run_on`](crate::app::run_on) is equivalent to `run_on_with(term, app,
297/// RunOptions::default())`. Pass [`RunOptions::animated`](crate::app::RunOptions::animated) for a continuously-rendering loop
298/// capped at a fixed rate instead, sleeping out the remainder of each frame's budget so
299/// `update` is called at even intervals rather than however fast the host can spin.
300///
301/// With [`RunOptions::is_event_driven`](crate::app::RunOptions::is_event_driven) `true` (the default), [`Flow::Idle`](crate::app::Flow::Idle) blocks the loop on
302/// input (via [`Terminal::wait_for_input`](crate::terminal::Terminal::wait_for_input)) instead of calling `update` again immediately:
303/// an idle app has nothing new to show, so there is no reason to burn CPU polling it at all,
304/// let alone faster than any configured rate. With `event_driven` `false`, an idle loop still
305/// waits out the remainder of the current `target_fps` interval (if set) before calling `update`
306/// again, rather than looping immediately, but never blocks on input.
307///
308/// # Errors
309///
310/// Returns the backend's error if the automatic `present()` call fails. The loop stops and the
311/// terminal is dropped (running backend teardown) before the error is returned.
312#[cfg(feature = "std")]
313pub fn run_on_with<B, A>(
314    mut term: Terminal<B>,
315    mut app: A,
316    options: RunOptions,
317) -> Result<(), B::Error>
318where
319    B: Backend,
320    A: App<B>,
321{
322    app.init(&mut term);
323    // `Some(0)` has no finite pacing interval to express, so it falls back to uncapped rather
324    // than computing `Duration::from_secs_f64(f64::INFINITY)` (which panics); see
325    // `WindowConfig`'s windowed driver for the same fallback.
326    let frame_budget = options
327        .target_fps()
328        .filter(|&fps| fps != 0)
329        .map(|fps| Duration::from_secs_f64(1.0 / f64::from(fps)));
330    let mut frame_count = 0u64;
331    let mut last = std::time::Instant::now();
332    loop {
333        // Block out the rest of this frame's budget before ticking `update` again, so a paced
334        // loop doesn't busy-spin between updates the way an uncapped one does.
335        if let Some(budget) = frame_budget
336            && let Some(remaining) = budget.checked_sub(last.elapsed())
337        {
338            std::thread::sleep(remaining);
339        }
340        let now = std::time::Instant::now();
341        let delta = now.duration_since(last);
342        last = now;
343        let frame = Frame::new(delta, frame_count);
344        frame_count = frame_count.wrapping_add(1);
345        let present_count_before = term.present_count();
346        let flow = app.update(&mut term, &frame);
347        if flow == Flow::Exit {
348            return Ok(());
349        }
350        // A no-op if `update` already called `present()` itself (detected via `present_count`
351        // rather than relying on `present()` being a safe no-op to call twice: it always presents
352        // unconditionally, so a second call here would diff the just-cleared `current` against
353        // the just-presented `previous` and erase the frame `update` already sent).
354        if flow != Flow::Idle && term.present_count() == present_count_before {
355            term.present()?;
356        }
357        // `Flow` is `#[non_exhaustive]`; treat any variant other than `Exit`/`Idle` the same as
358        // `Continue` (keep looping and presenting) rather than exiting on an unknown future value.
359        if flow == Flow::Idle
360            && let Idle::Block { wake } = options.idle()
361        {
362            // The heart of the fix for retroglyph#603: block here instead of immediately
363            // re-entering the loop, so an idle frame costs approximately nothing rather than
364            // spinning `update` as fast as the host allows. `wait_for_input` buffers any event it
365            // finds rather than consuming it, so the app's own `update` still observes it on the
366            // next iteration; this call only answers "did something happen", it doesn't steal
367            // the event. A `target_fps` clock (if set) still gets its top-of-loop sleep on the
368            // next iteration; it isn't bypassed by waking early.
369            term.wait_for_input(wake.unwrap_or(Duration::MAX));
370        }
371    }
372}
373
374/// Builds a [`Terminal`](crate::terminal::Terminal) over `backend` and drives `app` with
375/// [`run_on`](crate::app::run_on).
376///
377/// The canonical entry point for a blocking-loop backend (`Crossterm` in
378/// `retroglyph-crossterm`, [`Headless`](crate::backend::Headless), and any future backend with a
379/// loop it can enter and return from): construct the backend, hand it here with the app, and the
380/// terminal is built and owned for you. Backends whose control flow is inverted (winit owns the
381/// loop) or push-driven (wasm's `requestAnimationFrame`) keep their own drivers instead; see
382/// this module's doc comment for that split.
383///
384/// # Errors
385///
386/// Returns `backend`'s error if it fails to build a [`Terminal`](crate::terminal::Terminal) over
387/// itself, or if the automatic `present()` call fails while `app` is running. See
388/// [`run_on`](crate::app::run_on) for the exact loop behavior.
389#[cfg(feature = "std")]
390pub fn run<B, A>(backend: B, app: A) -> Result<(), B::Error>
391where
392    B: Backend,
393    A: App<B>,
394{
395    run_on(Terminal::new(backend), app)
396}
397
398/// A backend's own entry point for driving an [`App`](crate::app::App), named at the call site
399/// rather than selected by which Cargo features happen to be enabled.
400///
401/// Each backend crate implements this on whichever type already gathers the configuration it
402/// needs to start: `CrosstermOptions` in `retroglyph-crossterm` (reachable via
403/// `Crossterm::builder()`), and a small `Windowed` wrapper around each windowed backend's
404/// `PresenterBuilder` in `retroglyph-window` (reachable via `retroglyph-software`,
405/// `retroglyph-gl`, and `retroglyph-wgpu`). Both shapes let two backends live in the same binary
406/// with no conflict, since Cargo features are additive: a `run()` dispatched by `#[cfg(feature =
407/// "crossterm")]` would silently change which backend a binary runs the moment any dependency
408/// anywhere in the graph turned that feature on for its own reasons, and `--all-features` would
409/// only ever type-check one arm of it. Naming the concrete backend in code, via `Launch::launch`,
410/// makes every additive feature harmless instead.
411///
412/// A single generic `fn run<A>(app: A) where A: for<B: Backend> App<B>` isn't expressible on
413/// stable Rust either way (no non-lifetime binders; see rust#108185), so there was never a way to
414/// accept "an app that runs on any backend" from one function signature. Each `Launch` impl names
415/// one concrete [`Backend`]; an app written as `impl<B: Backend> App<B> for MyGame` satisfies
416/// every one of them without change.
417///
418/// This trait intentionally has no unified error type spanning every backend: [`launch`](Self::launch)
419/// returns [`Self::Error`], whatever the implementing backend's own error is (`std::io::Error`
420/// for crossterm, a small enum spanning the presenter builder's error and winit's
421/// `EventLoopError` for the windowed backends). A facade crate that depends on more than one
422/// backend and wants one error type spanning all of them can wrap this trait; this crate, which
423/// only ever sees one backend's impl at a time, does not.
424///
425/// Unlike [`run`](crate::app::run)/[`run_on`](crate::app::run_on) and their `_with` counterparts,
426/// this trait is not gated behind the `std` feature: the shape declared here (an associated
427/// `Backend`, an associated `Error`, and `launch`'s signature) uses nothing from `std`, only
428/// [`Backend`], [`App`], and [`RunOptions`], all of which are already available without it. Only
429/// the concrete impls need `std` in practice (`retroglyph-crossterm`'s and
430/// `retroglyph-window`'s both do, since they delegate to `run_on_with`/`run_app_on`), and each of
431/// those lives in its own backend crate, not here; nothing stops a future `no_std` backend from
432/// implementing `Launch` too.
433///
434/// # Examples
435///
436/// ```no_run
437/// use retroglyph_core::app::{App, Flow, Frame, Launch, RunOptions};
438/// use retroglyph_core::backend::Backend;
439/// use retroglyph_core::terminal::Terminal;
440///
441/// // A game written once, generic over the backend, satisfies every `Launch` impl unchanged.
442/// struct MyGame;
443/// impl<B: Backend> App<B> for MyGame {
444///     fn update(&mut self, _term: &mut Terminal<B>, _frame: &Frame) -> Flow {
445///         Flow::Exit
446///     }
447/// }
448///
449/// // Stand-ins for two real backends' entry points: `retroglyph-crossterm`'s
450/// // `CrosstermOptions` (reachable via `Crossterm::builder()`) and `retroglyph-window`'s
451/// // `Windowed<B>` (reachable via `retroglyph-software`/`-gl`/`-wgpu`). Each names one concrete
452/// // `Backend`, so both can `launch(MyGame, ..)` unmodified -- see those crates' own docs for
453/// // the real impls this sketches. Written by hand here (rather than delegating to
454/// // `run_on_with`, which both real impls actually use) purely so this doctest itself stays
455/// // `std`-free, proving `Launch` doesn't need it -- the real impls' own docs are the proof they
456/// // work end to end.
457/// # use retroglyph_core::backend::Headless;
458/// fn drive_to_exit<A: App<Headless> + 'static>(mut app: A) -> Result<(), core::convert::Infallible> {
459///     let mut term = Terminal::new(Headless::new(80, 24));
460///     let mut frame_count = 0u64;
461///     loop {
462///         let frame = Frame::new(core::time::Duration::ZERO, frame_count);
463///         frame_count += 1;
464///         if app.update(&mut term, &frame) == Flow::Exit {
465///             return Ok(());
466///         }
467///     }
468/// }
469///
470/// struct TerminalOptions;
471/// impl Launch for TerminalOptions {
472///     type Backend = Headless;
473///     type Error = core::convert::Infallible;
474///     fn launch<A>(self, app: A, _options: RunOptions) -> Result<(), Self::Error>
475///     where
476///         A: App<Self::Backend> + 'static,
477///     {
478///         drive_to_exit(app)
479///     }
480/// }
481///
482/// struct WindowedOptions;
483/// impl Launch for WindowedOptions {
484///     type Backend = Headless;
485///     type Error = core::convert::Infallible;
486///     fn launch<A>(self, app: A, _options: RunOptions) -> Result<(), Self::Error>
487///     where
488///         A: App<Self::Backend> + 'static,
489///     {
490///         drive_to_exit(app)
491///     }
492/// }
493///
494/// # fn call_sites() -> Result<(), core::convert::Infallible> {
495/// TerminalOptions.launch(MyGame, RunOptions::default())?;
496/// WindowedOptions.launch(MyGame, RunOptions::animated(60))?;
497/// # Ok(())
498/// # }
499/// ```
500pub trait Launch {
501    /// The backend this impl launches [`App`](crate::app::App) on.
502    type Backend: Backend;
503    /// The error this impl's [`launch`](Self::launch) can fail with; each backend surfaces its
504    /// own, unwrapped (see this trait's docs for why there is no unified error here).
505    type Error;
506
507    /// Builds this backend and drives `app` on it with `options`, blocking until `app` returns
508    /// [`Flow::Exit`](crate::app::Flow::Exit).
509    ///
510    /// # Errors
511    ///
512    /// Returns [`Self::Error`] if this backend fails to build, or if driving `app` fails once
513    /// running; see the implementing type's own docs for the exact conditions.
514    fn launch<A>(self, app: A, options: RunOptions) -> Result<(), Self::Error>
515    where
516        A: App<Self::Backend> + 'static;
517}
518
519/// Builds a [`Terminal`](crate::terminal::Terminal) over `backend` and drives `app` with
520/// [`run_on_with`](crate::app::run_on_with), paced by `options`.
521///
522/// The `options`-taking counterpart to [`run`](crate::app::run); see that function for which
523/// backends this suits, and [`RunOptions`](crate::app::RunOptions) for the available pacing and
524/// idle-blocking controls.
525///
526/// # Errors
527///
528/// Returns `backend`'s error if it fails to build a [`Terminal`](crate::terminal::Terminal) over
529/// itself, or if the automatic `present()` call fails while `app` is running. See
530/// [`run_on_with`](crate::app::run_on_with) for the exact loop behavior.
531#[cfg(feature = "std")]
532pub fn run_with<B, A>(backend: B, app: A, options: RunOptions) -> Result<(), B::Error>
533where
534    B: Backend,
535    A: App<B>,
536{
537    run_on_with(Terminal::new(backend), app, options)
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::backend::Headless;
544    use crate::event::{Event, KeyCode, KeyEvent, KeyModifiers};
545
546    struct Counter {
547        frames: u64,
548    }
549
550    impl App<Headless> for Counter {
551        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
552            self.frames += 1;
553            term.surface()
554                .put((0, 0), '#', crate::color::Style::default());
555            term.present().expect("present");
556            // Quit when a key is pending, or after a safety cap.
557            if term.has_input() || frame.frame >= 100 {
558                Flow::Exit
559            } else {
560                Flow::Continue
561            }
562        }
563    }
564
565    #[cfg(feature = "std")]
566    #[test]
567    fn run_on_exits_on_flow_exit() {
568        let mut backend = Headless::new(4, 1);
569        backend.push_event(Event::Key(KeyEvent::new(
570            KeyCode::Char('q'),
571            KeyModifiers::NONE,
572        )));
573        let term = Terminal::new(backend);
574        let app = Counter { frames: 0 };
575        // Runs until the queued key is observed. Reaching the next line proves
576        // the loop terminated on Flow::Exit rather than spinning forever.
577        run_on(term, app).expect("run_on");
578    }
579
580    /// Records the backend size [`App::init`](crate::app::App::init) saw and how many times it
581    /// ran, plus whether `update` had already run by the time `init` fired -- enough to prove
582    /// `init` runs exactly once, before the first `update`, with a live `term` already sized.
583    struct RecordsInit<'a> {
584        recorded_size: &'a mut Option<crate::grid::Size>,
585        init_calls: &'a mut u32,
586        update_ran_before_init: &'a mut bool,
587    }
588
589    impl App<Headless> for RecordsInit<'_> {
590        fn init(&mut self, term: &mut Terminal<Headless>) {
591            *self.init_calls += 1;
592            *self.recorded_size = Some(term.size());
593        }
594
595        fn update(&mut self, _term: &mut Terminal<Headless>, _frame: &Frame) -> Flow {
596            if *self.init_calls == 0 {
597                *self.update_ran_before_init = true;
598            }
599            Flow::Exit
600        }
601    }
602
603    #[cfg(feature = "std")]
604    #[test]
605    fn run_on_calls_init_once_before_the_first_update_with_the_real_size() {
606        let mut recorded_size = None;
607        let mut init_calls = 0;
608        let mut update_ran_before_init = false;
609        let term = Terminal::new(Headless::new(9, 5));
610        run_on(
611            term,
612            RecordsInit {
613                recorded_size: &mut recorded_size,
614                init_calls: &mut init_calls,
615                update_ran_before_init: &mut update_ran_before_init,
616            },
617        )
618        .expect("run_on");
619        assert_eq!(recorded_size, Some(crate::grid::Size::new(9, 5)));
620        assert_eq!(init_calls, 1);
621        assert!(!update_ran_before_init);
622    }
623
624    /// An app that never draws and always returns `Idle` except on the last frame: proves
625    /// `run_on` skips `present()` for `Idle` frames rather than erasing an untouched grid.
626    struct AlwaysIdle {
627        frames: u64,
628    }
629
630    impl App<Headless> for AlwaysIdle {
631        fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
632            self.frames += 1;
633            if frame.frame >= 5 {
634                Flow::Exit
635            } else {
636                Flow::Idle
637            }
638        }
639    }
640
641    #[cfg(feature = "std")]
642    #[test]
643    fn run_on_skips_present_on_idle() {
644        let term = Terminal::new(Headless::new(2, 1));
645        let app = AlwaysIdle { frames: 0 };
646        // `update` never draws or presents; if the driver called `present()` on an `Idle` frame
647        // anyway it would be harmless here (nothing to erase), so this mainly documents intent --
648        // the presenting behavior itself is covered by `run_on_with_options_presents_frames`.
649        run_on(term, app).expect("run_on");
650    }
651
652    /// An app that draws a distinct glyph per frame and never presents itself, so successfully
653    /// reaching the backend proves the driver's automatic present ran.
654    struct DrawsAndExits {
655        frames: u64,
656        exit_at: u64,
657    }
658
659    impl App<Headless> for DrawsAndExits {
660        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
661            self.frames += 1;
662            term.surface()
663                .put((0, 0), 'x', crate::color::Style::default());
664            if frame.frame >= self.exit_at {
665                Flow::Exit
666            } else {
667                Flow::Continue
668            }
669        }
670    }
671
672    #[cfg(feature = "std")]
673    #[test]
674    fn run_on_presents_automatically() {
675        let term = Terminal::new(Headless::new(2, 1));
676        let app = DrawsAndExits {
677            frames: 0,
678            exit_at: 0,
679        };
680        run_on(term, app).expect("run_on");
681        // No assertion on backend content is possible here: `term` is consumed by `run_on`.
682        // Coverage that the automatic present actually reaches the backend lives in
683        // `retroglyph-window`'s own driver tests, which retain the terminal after the loop.
684    }
685
686    #[cfg(feature = "std")]
687    #[test]
688    fn run_on_with_default_options_matches_run_on() {
689        let term = Terminal::new(Headless::new(2, 1));
690        let app = DrawsAndExits {
691            frames: 0,
692            exit_at: 2,
693        };
694        run_on_with(term, app, RunOptions::default()).expect("run_on_with");
695    }
696
697    #[cfg(feature = "std")]
698    #[test]
699    fn run_on_with_animated_options_runs_to_completion() {
700        let term = Terminal::new(Headless::new(2, 1));
701        let app = DrawsAndExits {
702            frames: 0,
703            exit_at: 2,
704        };
705        // A high cap keeps this test fast; the point is that a paced loop still terminates on
706        // `Flow::Exit` and delivers the same number of updates as an uncapped loop would.
707        run_on_with(term, app, RunOptions::animated(1000)).expect("run_on_with");
708    }
709
710    #[cfg(feature = "std")]
711    #[test]
712    fn run_on_with_animated_options_paces_updates_to_the_target_fps() {
713        // Regression guard for retroglyph#1393: the frame budget replacing `FrameClock` must
714        // still sleep out the remainder of each frame, not just derive a step it never sleeps on.
715        let term = Terminal::new(Headless::new(2, 1));
716        let app = DrawsAndExits {
717            frames: 0,
718            exit_at: 4,
719        };
720        let start = std::time::Instant::now();
721        // 50 fps = 20ms/frame; 5 updates (0..=4) means 4 sleeps between them.
722        run_on_with(term, app, RunOptions::animated(50)).expect("run_on_with");
723        assert!(start.elapsed() >= Duration::from_millis(4 * 20));
724    }
725
726    #[cfg(feature = "std")]
727    #[test]
728    fn run_on_with_target_fps_zero_runs_uncapped_instead_of_panicking() {
729        // Regression guard for retroglyph#1448: `Some(0)` used to panic deriving a frame budget
730        // from it; it now falls back to uncapped, matching the windowed driver.
731        let term = Terminal::new(Headless::new(2, 1));
732        let app = DrawsAndExits {
733            frames: 0,
734            exit_at: 2,
735        };
736        run_on_with(term, app, RunOptions::animated(0)).expect("run_on_with");
737    }
738
739    #[cfg(feature = "std")]
740    #[test]
741    fn run_builds_the_terminal_and_exits_on_flow_exit() {
742        let mut backend = Headless::new(4, 1);
743        backend.push_event(Event::Key(KeyEvent::new(
744            KeyCode::Char('q'),
745            KeyModifiers::NONE,
746        )));
747        let app = Counter { frames: 0 };
748        // `run` takes the bare backend rather than a `Terminal`, unlike `run_on`; reaching
749        // the next line proves it still builds one and drives the loop to `Flow::Exit`.
750        run(backend, app).expect("run");
751    }
752
753    #[cfg(feature = "std")]
754    #[test]
755    fn run_with_builds_the_terminal_and_honors_options() {
756        let backend = Headless::new(2, 1);
757        let app = DrawsAndExits {
758            frames: 0,
759            exit_at: 2,
760        };
761        // Same proof as `run_on_with_animated_options_runs_to_completion`, but starting
762        // from a bare backend to cover `run_with`'s own `Terminal::new` call.
763        run_with(backend, app, RunOptions::animated(1000)).expect("run_with");
764    }
765
766    #[test]
767    fn run_options_animated_sets_fields() {
768        let animated = RunOptions::animated(30);
769        assert_eq!(animated.target_fps(), Some(30));
770        assert_eq!(animated.idle(), Idle::Spin);
771        assert!(!animated.is_event_driven());
772        assert_eq!(animated.idle_wake(), None);
773
774        let default = RunOptions::default();
775        assert_eq!(default.target_fps(), None);
776        assert_eq!(default.idle(), Idle::Block { wake: None });
777        assert!(default.is_event_driven());
778        assert_eq!(default.idle_wake(), None);
779    }
780
781    #[test]
782    fn run_options_setters_override_defaults() {
783        // `with_idle_wake` after `event_driven(false)` switches back to `Idle::Block`: builder
784        // calls apply in order, so this is no longer the dead combination it used to be.
785        let options = RunOptions::default()
786            .with_target_fps(60)
787            .event_driven(false)
788            .with_idle_wake(Duration::from_millis(250));
789        assert_eq!(options.target_fps(), Some(60));
790        assert_eq!(
791            options.idle(),
792            Idle::Block {
793                wake: Some(Duration::from_millis(250))
794            }
795        );
796        assert!(options.is_event_driven());
797        assert_eq!(options.idle_wake(), Some(Duration::from_millis(250)));
798    }
799
800    #[test]
801    fn run_options_event_driven_false_switches_to_spin() {
802        let options = RunOptions::default().event_driven(false);
803        assert_eq!(options.idle(), Idle::Spin);
804        assert!(!options.is_event_driven());
805        assert_eq!(options.idle_wake(), None);
806    }
807
808    /// An app that returns `Idle` for its first frame, then `Exit`. The queued key is only
809    /// pushed into the backend *after* the driver would have already woken from the idle wait
810    /// (`Headless::poll_event` ignores its timeout and returns immediately either way), so this
811    /// mainly documents the contract at the type level: `event_driven: false` is accepted and the
812    /// loop still terminates, i.e. the non-blocking `Idle` shape is a supported option for
813    /// animated apps. Real blocking behavior (`event_driven: true` actually parking
814    /// the thread) can only be observed on a backend that genuinely blocks, like crossterm --
815    /// see that crate's own tests.
816    struct IdleThenExit {
817        frames: u64,
818    }
819
820    impl App<Headless> for IdleThenExit {
821        fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
822            self.frames += 1;
823            if frame.frame == 0 {
824                Flow::Idle
825            } else {
826                Flow::Exit
827            }
828        }
829    }
830
831    #[cfg(feature = "std")]
832    #[test]
833    fn run_on_with_non_event_driven_options_does_not_block_on_idle() {
834        let term = Terminal::new(Headless::new(2, 1));
835        let app = IdleThenExit { frames: 0 };
836        let options = RunOptions {
837            target_fps: None,
838            idle: Idle::Spin,
839        };
840        run_on_with(term, app, options).expect("run_on_with");
841    }
842
843    /// Proves the driver's idle wait doesn't swallow the event it woke up for: `update` is only
844    /// ever called again *after* `wait_for_input` observed something, so the app's own `has_input`
845    /// must still see the same event on the next frame rather than the driver having consumed it.
846    struct ObservesQueuedEventAfterIdle {
847        frames: u64,
848        saw_input_after_idle: bool,
849    }
850
851    impl App<Headless> for ObservesQueuedEventAfterIdle {
852        fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
853            self.frames += 1;
854            if frame.frame == 0 {
855                return Flow::Idle;
856            }
857            self.saw_input_after_idle = term.has_input();
858            Flow::Exit
859        }
860    }
861
862    #[test]
863    fn run_on_event_driven_idle_wait_does_not_consume_the_waking_event() {
864        let mut backend = Headless::new(2, 1);
865        backend.push_event(Event::Key(KeyEvent::new(
866            KeyCode::Char('x'),
867            KeyModifiers::NONE,
868        )));
869        let term = Terminal::new(backend);
870        let mut app = ObservesQueuedEventAfterIdle {
871            frames: 0,
872            saw_input_after_idle: false,
873        };
874        // Can't recover `app` through `run_on` (it takes the app by value and drops it with
875        // the terminal), so drive the loop by hand via `step`, mirroring what `run_on_with`
876        // does around the `Flow::Idle` branch.
877        let mut term = term;
878        let frame0 = Frame::new(Duration::ZERO, 0);
879        assert_eq!(app.update(&mut term, &frame0), Flow::Idle);
880        // This is the exact call `run_on_with` makes on `Flow::Idle` when `event_driven` is
881        // `true`: it must buffer the event, not return/consume it, so `update`'s own `has_input`
882        // still finds it below.
883        assert!(term.wait_for_input(Duration::MAX));
884        let frame1 = Frame::new(Duration::ZERO, 1);
885        assert_eq!(app.update(&mut term, &frame1), Flow::Exit);
886        assert!(app.saw_input_after_idle);
887    }
888}