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}