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;