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