Skip to main content

shuyi_ui/
screen.rs

1//! Screens and the screen stack: the shape every screen of a game has.
2//!
3//! A [`Screen`] reads the frame's actions ([`FrameInput`]) in
4//! [`update`](Screen::update) and answers with a [`Transition`]; it paints
5//! itself into a [`GlyphBuffer`] in [`draw`](Screen::draw). The
6//! [`ScreenStack`] routes input to the top screen only and draws from the
7//! top-most opaque screen up, so overlays (menus, dialogs) show the screen
8//! below them. Screens are generic over the game's part of the context
9//! ([`GameData`]), which also gives their actions.
10//!
11//! A screen can ask something on a screen of its own and get the answer
12//! back: it pushes the question, which pops with a value
13//! ([`Transition::pop_with`]), and the stack hands that value to the
14//! screen below in [`uncovered`](Screen::uncovered). A screen also hears
15//! when one is pushed over it ([`covered`](Screen::covered)) and when that
16//! one goes, with or without a value.
17//!
18//! A screen declares the actions it reacts to ([`actions`](Screen::actions),
19//! ADR-0008 §3), so `shuyi check --target` can tell whether its required
20//! ones fit a target's buttons. To keep the declarations honest, a stack
21//! with its [guard](ScreenStack::guard_actions) on (the test Harness's)
22//! hands a screen only the actions it declares, and logs the others
23//! ([`ScreenStack::undeclared`]).
24//!
25//! A sound started with a handle ([`AudioQueue::start_sound`]) belongs to
26//! the screen that started it: when that screen leaves the stack through
27//! [`ScreenStack::apply_in`], the stack stops it, so a loop can't outlive
28//! its screen. A sound started from outside any screen belongs to the top
29//! one.
30//!
31//! [`AudioQueue::start_sound`]: crate::audio::AudioQueue::start_sound
32
33use crate::prelude::*;
34use core::any::Any;
35use core::fmt;
36
37use shuyi_core::StateHash;
38
39use crate::ctx::{Ctx, GameData};
40use crate::debug::gizmo::Gizmos;
41use crate::frame::glyph_buffer::GlyphBuffer;
42
43pub use crate::input::frame::FrameInput;
44
45/// One screen of a game: a title, a map, a menu overlay, …
46pub trait Screen<G: GameData> {
47    /// A stable, unique `snake_case` name, for tests and debugging.
48    fn name(&self) -> &'static str;
49
50    /// Handles one frame: `input` holds this frame's actions (often none)
51    /// and elapsed time. Called only while this screen is on top.
52    fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> Transition<G>;
53
54    /// Paints the screen. An opaque screen must cover the whole buffer.
55    fn draw(&self, ctx: &Ctx<G>, buf: &mut GlyphBuffer);
56
57    /// Hands in what the debug view shows of this screen (ADR-0085): its
58    /// gizmos, each in its channel, in console pixels, built from `self`
59    /// and `ctx` alone, so looking changes nothing. A channel's helper
60    /// (the grid kit's [`layout_gizmos`](crate::grid::debug::layout_gizmos))
61    /// does the work. Called after [`draw`](Self::draw) on every screen
62    /// drawn, bottom up, only while debug tools are on
63    /// ([`Ctx::debug_tools`]). Does nothing unless the screen has
64    /// something to show.
65    fn debug_draw(&self, ctx: &Ctx<G>, out: &mut Gizmos) {
66        let _ = (ctx, out);
67    }
68
69    /// Whether the screen below shows through (drawn first, then this one).
70    fn is_overlay(&self) -> bool {
71        false
72    }
73
74    /// The actions this screen reacts to, in any order (ADR-0008 §3):
75    /// every one it reads from its input, through [`ActionSet::ui`] or
76    /// not. `None`, the default, has not declared them, and counts as
77    /// every action of the game: `shuyi check --target` warns about it.
78    /// An overlay declares its own, and is checked on its own.
79    ///
80    /// [`ui_actions`](crate::input::ui_actions) gives a game's actions for
81    /// the [`UiAction`](crate::input::UiAction)s an engine screen or
82    /// widget reacts to.
83    ///
84    /// [`ActionSet::ui`]: crate::input::ActionSet::ui
85    fn actions(&self) -> Option<Vec<G::Action>> {
86        None
87    }
88
89    /// The hash of the game's state this screen holds, which goes in each
90    /// frame's hash in a session record ([`Game::state_hash`]), most simply
91    /// `shuyi_core::state_hash(&self.rules).ok()`: so a playback that plays
92    /// differently is found at the frame the state moved. `None` unless the
93    /// screen opts in.
94    ///
95    /// [`Game::state_hash`]: crate::Game::state_hash
96    fn state_hash(&self) -> Option<StateHash> {
97        None
98    }
99
100    /// This screen as [`Any`], for tests that look inside a screen on the
101    /// stack ([`ScreenStack::find`]). `None` unless the screen opts in.
102    fn as_any(&self) -> Option<&dyn Any> {
103        None
104    }
105
106    /// [`as_any`](Self::as_any), mutable ([`ScreenStack::find_mut`]).
107    fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
108        None
109    }
110
111    /// Called when a screen is pushed on top of this one
112    /// ([`Transition::Push`]): it is no longer updated until it is
113    /// [`uncovered`](Self::uncovered). Not called when the screen above
114    /// swaps itself for another ([`Transition::Replace`]). Does nothing
115    /// unless the screen needs it.
116    fn covered(&mut self, _ctx: &mut Ctx<G>) {}
117
118    /// Called when the screen above pops and this one is on top again:
119    /// `result` is what it popped with ([`Transition::PopWith`]), `None`
120    /// after a plain [`Transition::Pop`]. The transition returned is
121    /// applied at once, as if from [`update`](Self::update), so a screen
122    /// can act on the answer in the same frame: push the next screen, or
123    /// pop with an answer of its own. By default, nothing happens.
124    ///
125    /// ```
126    /// # use shuyi_ui::screen::ScreenResult;
127    /// // A question screen answers yes…
128    /// let result = Some(ScreenResult::new(true));
129    /// // …and the screen below reads it as the type the two agree on.
130    /// let yes = result.and_then(ScreenResult::take::<bool>) == Some(true);
131    /// assert!(yes);
132    /// ```
133    fn uncovered(&mut self, ctx: &mut Ctx<G>, result: Option<ScreenResult>) -> Transition<G> {
134        let _ = (ctx, result);
135        Transition::None
136    }
137}
138
139/// A value a screen pops with ([`Transition::PopWith`]) for the screen
140/// below, which reads it back as its type in [`Screen::uncovered`]. Any
141/// `'static` type will do; the two screens agree on which.
142pub struct ScreenResult {
143    value: Box<dyn Any>,
144    type_name: &'static str,
145}
146
147impl ScreenResult {
148    /// `value`, to hand to the screen below.
149    pub fn new<T: Any>(value: T) -> Self {
150        Self {
151            value: Box::new(value),
152            type_name: core::any::type_name::<T>(),
153        }
154    }
155
156    /// Whether the value is a `T`.
157    pub fn is<T: Any>(&self) -> bool {
158        self.value.is::<T>()
159    }
160
161    /// The value, if it is a `T`.
162    pub fn get<T: Any>(&self) -> Option<&T> {
163        self.value.downcast_ref()
164    }
165
166    /// The value, if it is a `T`; dropped if it isn't.
167    pub fn take<T: Any>(self) -> Option<T> {
168        self.downcast().ok()
169    }
170
171    /// The value, if it is a `T`.
172    ///
173    /// # Errors
174    ///
175    /// The result itself, unchanged, when its value is not a `T`.
176    pub fn downcast<T: Any>(self) -> Result<T, Self> {
177        let type_name = self.type_name;
178        self.value
179            .downcast()
180            .map(|value| *value)
181            .map_err(|value| Self { value, type_name })
182    }
183}
184
185// check-text: not player text (debug output)
186impl fmt::Debug for ScreenResult {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        write!(f, "ScreenResult({})", self.type_name)
189    }
190}
191
192/// What a screen asks the stack to do after an update.
193pub enum Transition<G: GameData> {
194    /// Stay on this screen.
195    None,
196    /// Put a new screen on top of this one.
197    Push(Box<dyn Screen<G>>),
198    /// Remove this screen, returning to the one below (or quitting if it was
199    /// the last).
200    Pop,
201    /// [`Pop`](Self::Pop), handing a value to the screen below
202    /// ([`Screen::uncovered`]). [`Transition::pop_with`] makes one.
203    PopWith(ScreenResult),
204    /// Swap this screen for another.
205    Replace(Box<dyn Screen<G>>),
206    /// Quit the game.
207    Quit,
208}
209
210impl<G: GameData> Transition<G> {
211    /// [`Transition::PopWith`] `value`.
212    pub fn pop_with<T: Any>(value: T) -> Self {
213        Self::PopWith(ScreenResult::new(value))
214    }
215}
216
217// check-text: not player text (debug output)
218impl<G: GameData> fmt::Debug for Transition<G> {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::None => f.write_str("None"),
222            Self::Push(s) => write!(f, "Push({})", s.name()),
223            Self::Pop => f.write_str("Pop"),
224            Self::PopWith(result) => write!(f, "PopWith({})", result.type_name),
225            Self::Replace(s) => write!(f, "Replace({})", s.name()),
226            Self::Quit => f.write_str("Quit"),
227        }
228    }
229}
230
231/// The stack of open screens. Only the top screen is updated; drawing starts
232/// at the top-most opaque screen and goes up through the overlays above it.
233pub struct ScreenStack<G: GameData> {
234    screens: Vec<Box<dyn Screen<G>>>,
235    /// Whether a screen gets only the actions it declares
236    /// ([`guard_actions`](Self::guard_actions)).
237    guard: bool,
238    /// The actions held back from a screen that doesn't declare them,
239    /// oldest first, with the screen's name.
240    undeclared: Vec<(&'static str, G::Action)>,
241}
242
243impl<G: GameData> Default for ScreenStack<G> {
244    fn default() -> Self {
245        Self {
246            screens: Vec::new(),
247            guard: false,
248            undeclared: Vec::new(),
249        }
250    }
251}
252
253impl<G: GameData> ScreenStack<G> {
254    /// A stack holding just `root`.
255    pub fn new(root: Box<dyn Screen<G>>) -> Self {
256        Self {
257            screens: vec![root],
258            ..Self::default()
259        }
260    }
261
262    /// Turns the guard on or off: while it is on, a screen that declares
263    /// its actions ([`Screen::actions`]) is handed only those
264    /// ([`FrameInput::declared`]), and the others are logged
265    /// ([`undeclared`](Self::undeclared)). So a test of a screen that reacts
266    /// to an action it didn't declare fails. The test Harness turns it on;
267    /// a game never needs to.
268    pub fn guard_actions(&mut self, on: bool) {
269        self.guard = on;
270    }
271
272    /// Every action held back from a screen that doesn't declare it while
273    /// the [guard](Self::guard_actions) was on, oldest first, with the
274    /// screen's name. A player pressing a key the screen has no use for
275    /// lands here too: it says why a screen didn't react, not that it is
276    /// wrong.
277    pub fn undeclared(&self) -> &[(&'static str, G::Action)] {
278        &self.undeclared
279    }
280
281    /// Whether no screen is open (the game should quit).
282    pub fn is_empty(&self) -> bool {
283        self.screens.is_empty()
284    }
285
286    /// Names of the open screens, bottom first.
287    pub fn names(&self) -> Vec<&'static str> {
288        self.screens.iter().map(|s| s.name()).collect()
289    }
290
291    /// Name of the top screen.
292    pub fn top_name(&self) -> Option<&'static str> {
293        self.screens.last().map(|s| s.name())
294    }
295
296    /// The actions the top screen declares it reacts to
297    /// ([`Screen::actions`]), if it declares them.
298    pub fn top_actions(&self) -> Option<Vec<G::Action>> {
299        self.screens.last().and_then(|s| s.actions())
300    }
301
302    /// Puts `screen` on top, to build a stack: the screen below is not
303    /// told ([`Screen::covered`]); [`apply_in`](Self::apply_in) with
304    /// [`Transition::Push`] tells it.
305    pub fn push(&mut self, screen: Box<dyn Screen<G>>) {
306        self.screens.push(screen);
307    }
308
309    /// The top-most screen of type `T` (one that opts in with
310    /// [`Screen::as_any`]).
311    pub fn find<T: Any>(&self) -> Option<&T> {
312        self.screens
313            .iter()
314            .rev()
315            .find_map(|s| s.as_any()?.downcast_ref())
316    }
317
318    /// The top-most screen's [`Screen::state_hash`] that has one.
319    pub fn state_hash(&self) -> Option<StateHash> {
320        self.screens.iter().rev().find_map(|s| s.state_hash())
321    }
322
323    /// The top-most screen of type `T`, mutable ([`Screen::as_any_mut`]).
324    pub fn find_mut<T: Any>(&mut self) -> Option<&mut T> {
325        self.screens
326            .iter_mut()
327            .rev()
328            .find_map(|s| s.as_any_mut()?.downcast_mut())
329    }
330
331    /// Updates the top screen and applies its transition
332    /// ([`apply_in`](Self::apply_in)). Returns `true` if the game should
333    /// quit ([`Transition::Quit`], or the last screen popped). Does nothing
334    /// and returns `true` on an empty stack.
335    pub fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> bool {
336        self.own_top(ctx);
337        let Some(top) = self.screens.last_mut() else {
338            return true;
339        };
340        let declared = self.guard.then(|| top.actions()).flatten();
341        let transition = match declared {
342            Some(declared) => {
343                let name = top.name();
344                let (input, missed) = input.declared(name, &declared);
345                self.undeclared
346                    .extend(missed.into_iter().map(|a| (name, a)));
347                top.update(ctx, &input)
348            }
349            None => top.update(ctx, input),
350        };
351        self.apply_in(ctx, transition)
352    }
353
354    /// Applies a transition as if the top screen had returned it, telling
355    /// the screens it covers and uncovers: a push calls the old top's
356    /// [`Screen::covered`]; a pop calls the new top's
357    /// [`Screen::uncovered`] with what the screen popped with, then applies
358    /// the transition that returns, and so on. A replace tells nobody.
359    /// A screen that leaves (a pop or a replace) takes its sounds with it
360    /// ([`AudioQueue::start_sound`]). Returns `true` if the game should
361    /// quit.
362    ///
363    /// [`AudioQueue::start_sound`]: crate::audio::AudioQueue::start_sound
364    pub fn apply_in(&mut self, ctx: &mut Ctx<G>, transition: Transition<G>) -> bool {
365        let mut next = transition;
366        let quit = loop {
367            let result = match next {
368                Transition::Pop => None,
369                Transition::PopWith(result) => Some(result),
370                Transition::Push(screen) => {
371                    self.own_top(ctx);
372                    if let Some(top) = self.screens.last_mut() {
373                        top.covered(ctx);
374                    }
375                    self.screens.push(screen);
376                    break false;
377                }
378                replace @ Transition::Replace(_) => {
379                    self.release_top(ctx);
380                    break self.apply(replace);
381                }
382                other => break self.apply(other),
383            };
384            self.release_top(ctx);
385            self.screens.pop();
386            self.own_top(ctx);
387            let Some(top) = self.screens.last_mut() else {
388                break true;
389            };
390            next = top.uncovered(ctx, result);
391        };
392        self.own_top(ctx);
393        quit
394    }
395
396    /// Makes the top screen the owner of the sounds started from now on.
397    pub(crate) fn own_top(&self, ctx: &mut Ctx<G>) {
398        ctx.audio.set_owner(self.screens.len().saturating_sub(1));
399    }
400
401    /// Stops the top screen's sounds: it is leaving.
402    fn release_top(&self, ctx: &mut Ctx<G>) {
403        if let Some(depth) = self.screens.len().checked_sub(1) {
404            ctx.audio.release(depth);
405        }
406    }
407
408    /// Applies a transition as if the top screen had returned it, to the
409    /// stack alone: no screen is told it is covered or uncovered, and a
410    /// value popped with is dropped ([`apply_in`](Self::apply_in) tells
411    /// them). Returns `true` if the game should quit.
412    pub fn apply(&mut self, transition: Transition<G>) -> bool {
413        match transition {
414            Transition::None => {}
415            Transition::Push(screen) => self.screens.push(screen),
416            Transition::Pop | Transition::PopWith(_) => {
417                self.screens.pop();
418            }
419            Transition::Replace(screen) => {
420                self.screens.pop();
421                self.screens.push(screen);
422            }
423            Transition::Quit => return true,
424        }
425        self.screens.is_empty()
426    }
427
428    /// Draws the top-most opaque screen and every overlay above it, bottom
429    /// up. If every screen is an overlay, all are drawn.
430    pub fn draw(&self, ctx: &Ctx<G>, buf: &mut GlyphBuffer) {
431        for screen in self.drawn() {
432            screen.draw(ctx, buf);
433        }
434    }
435
436    /// Asks every screen [`draw`](Self::draw) draws for its debug view's
437    /// gizmos ([`Screen::debug_draw`]), bottom up, so an overlay's come
438    /// after those of the screen under it.
439    pub fn debug_draw(&self, ctx: &Ctx<G>, out: &mut Gizmos) {
440        for screen in self.drawn() {
441            screen.debug_draw(ctx, out);
442        }
443    }
444
445    /// The screens drawn: the top-most opaque one and every overlay above
446    /// it (all of them if every one is an overlay).
447    fn drawn(&self) -> &[Box<dyn Screen<G>>] {
448        let base = self
449            .screens
450            .iter()
451            .rposition(|s| !s.is_overlay())
452            .unwrap_or(0);
453        &self.screens[base..]
454    }
455}
456
457#[cfg(test)]
458mod tests;