Skip to main content

shuyi_ui/
debug.rs

1//! The developer's debug screens: a menu of the game's tools
2//! ([`DebugMenuScreen`], with the game's and its modules'
3//! [`DebugEntry`]s), the glyph
4//! sampler that shows every glyph of the font and every color of the
5//! palette ([`GlyphSamplerScreen`]), the scene camera over a picture of
6//! the game's ([`SceneCameraScreen`]), and the hint that names the Debug
7//! key ([`hint`]). Debug tools are in English only.
8//!
9//! **The debug view** (ADR-0085) draws what a running game's logic sees
10//! over it, as Godot's "Visible Collision Shapes" does: each screen hands
11//! in [`gizmo::Gizmo`]s from
12//! [`Screen::debug_draw`], each in a
13//! [`gizmo::Channel`] with a [`gizmo::GizmoRole`], and the channels on
14//! ([`Ctx::debug_view`]) are drawn over the frame in the debug tools'
15//! colours ([`glyph::paint_gizmos`]). The menu's "Debug view" entry
16//! ([`DebugViewScreen`]) turns them on and off. The grid kit's layout is
17//! the `grid` channel ([`crate::grid::debug`]).
18//!
19//! **The look is a skin**: the menu says what it shows as a
20//! [`DebugMenuView`] and [`glyph::paint`] draws it; the scene camera's
21//! [`SceneCameraView`](camera::SceneCameraView), [`glyph::paint_camera`];
22//! the debug view's screen's [`DebugViewView`](view::DebugViewView),
23//! [`glyph::paint_view`].
24
25pub mod camera;
26pub mod gizmo;
27pub mod glyph;
28pub mod view;
29
30pub use camera::{CameraLayout, SceneCameraScreen};
31pub use gizmo::{Channel, DebugView, Gizmo, GizmoRole, Gizmos};
32pub use view::DebugViewScreen;
33
34use crate::prelude::*;
35use core::fmt;
36
37use shuyi_assets::keymap::{ActionSet, UiAction};
38
39use crate::audio::MenuSound;
40use crate::ctx::{Ctx, GameData};
41use crate::frame::color::Palette;
42use crate::frame::console::ConsoleSpec;
43use crate::frame::glyph_buffer::GlyphBuffer;
44use crate::input::help_keys::{HelpKeys, cursor_keys_name, help_line, key_name};
45use crate::screen::{FrameInput, Screen, Transition};
46use crate::widgets::{Menu, MenuEvent, MenuItem, MenuView};
47
48/// The debug-menu hint, e.g. `F2 debug`: `None` unless debug tools are on
49/// and the game's Debug action has a key (it isn't rebindable, so no
50/// `! not mapped` hint for it).
51pub fn hint<G: GameData>(ctx: &Ctx<G>) -> Option<String> {
52    let debug = G::Action::for_ui(UiAction::Debug)?;
53    let bound = ctx.keymap.primary(debug).is_some();
54    // The debug key is keyboard-only, so it is named on a controller too.
55    let keys = HelpKeys::keyboard(&ctx.keymap);
56    // check-text: not player text (debug tools are English only)
57    (ctx.debug_tools && bound).then(|| format!("{} debug", key_name(keys, debug)))
58}
59
60/// What choosing a debug tool does, given the context. Its own state (how
61/// many times it ran, say) lives in the closure.
62pub type Run<G> = Box<dyn FnMut(&mut Ctx<G>) -> Transition<G>>;
63
64/// What a debug tool is called on the menu, read from the context.
65pub type Label<G> = Box<dyn Fn(&Ctx<G>) -> String>;
66
67/// One tool on the debug menu: what the menu calls it (which may name
68/// something in `ctx`, such as a look in use) and what choosing it does.
69pub struct DebugEntry<G: GameData> {
70    /// Its name on the menu.
71    label: Label<G>,
72    /// What choosing it does.
73    run: Run<G>,
74}
75
76impl<G: GameData> DebugEntry<G> {
77    /// A tool called `label`.
78    pub fn new(
79        label: &'static str,
80        run: impl FnMut(&mut Ctx<G>) -> Transition<G> + 'static,
81    ) -> Self {
82        Self::named(move |_| label.to_owned(), run)
83    }
84
85    /// A tool whose name `label` reads from the context each time the menu
86    /// is drawn after it runs.
87    pub fn named(
88        label: impl Fn(&Ctx<G>) -> String + 'static,
89        run: impl FnMut(&mut Ctx<G>) -> Transition<G> + 'static,
90    ) -> Self {
91        Self {
92            label: Box::new(label),
93            run: Box::new(run),
94        }
95    }
96
97    /// Its name on the menu now.
98    pub fn label(&self, ctx: &Ctx<G>) -> String {
99        (self.label)(ctx)
100    }
101
102    /// Runs it.
103    pub fn run(&mut self, ctx: &mut Ctx<G>) -> Transition<G> {
104        (self.run)(ctx)
105    }
106}
107
108impl<G: GameData> fmt::Debug for DebugEntry<G> {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.debug_struct("DebugEntry").finish_non_exhaustive()
111    }
112}
113
114/// What the debug menu shows.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct DebugMenuView {
117    /// Its title.
118    pub title: String,
119    /// The tools.
120    pub menu: MenuView,
121    /// The help line.
122    pub help: String,
123}
124
125/// The debug menu the Debug key opens: the game's tools, in its order,
126/// then each installed module's ([`Install::debug_entry`](crate::module::Install::debug_entry)),
127/// in install order, then the engine's: while the platform records the
128/// run or plays a record ([`Ctx::session_recording`]), "Save session",
129/// and "Debug view" ([`DebugViewScreen`]). Cancel closes it. A tool that opens nothing leaves the menu on it, its names read
130/// again.
131pub struct DebugMenuScreen<G: GameData> {
132    /// Each item stands for its entry's index in `entries`.
133    menu: Menu<usize>,
134    entries: Vec<DebugEntry<G>>,
135}
136
137impl<G: GameData> fmt::Debug for DebugMenuScreen<G> {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.debug_struct("DebugMenuScreen")
140            .field("menu", &self.menu)
141            .field("entries", &self.entries.len())
142            .finish()
143    }
144}
145
146impl<G: GameData> DebugMenuScreen<G> {
147    /// Name reported by [`Screen::name`].
148    pub const NAME: &'static str = "debug_menu";
149
150    /// The menu of the game's `entries`, then the entries of the modules
151    /// installed on `ctx`, made now, then the engine's, its first tool
152    /// focused.
153    pub fn new(ctx: &Ctx<G>, mut entries: Vec<DebugEntry<G>>) -> Self {
154        entries.extend(ctx.modules.debug_entries());
155        if ctx.session_recording() {
156            // check-text: not player text (debug tools are English only)
157            entries.push(DebugEntry::new("Save session", |ctx| {
158                ctx.ask_session_save();
159                Transition::None
160            }));
161        }
162        // check-text: not player text (debug tools are English only)
163        entries.push(DebugEntry::new("Debug view", |ctx| {
164            Transition::Push(Box::new(DebugViewScreen::new(ctx)))
165        }));
166        let menu = Self::menu_of(ctx, &entries);
167        Self { menu, entries }
168    }
169
170    /// The menu of `entries`' names now.
171    fn menu_of(ctx: &Ctx<G>, entries: &[DebugEntry<G>]) -> Menu<usize> {
172        Menu::new(
173            entries
174                .iter()
175                .enumerate()
176                .map(|(i, e)| MenuItem::new(e.label(ctx)).with_value(i))
177                .collect(),
178        )
179    }
180
181    /// The menu: its items, each standing for its tool's index, and focus.
182    pub fn menu(&self) -> &Menu<usize> {
183        &self.menu
184    }
185
186    /// The same menu focused on tool `index`.
187    #[must_use]
188    pub fn focused(mut self, index: usize) -> Self {
189        self.menu = self.menu.focused(index);
190        self
191    }
192
193    /// What the menu shows: its title, the tools and the help line, naming
194    /// the keys of the device used last.
195    pub fn view(&self, ctx: &Ctx<G>) -> DebugMenuView {
196        let km = ctx.help_keys();
197        let key = |ui| G::Action::for_ui(ui).map(|a| key_name(km, a));
198        // check-text: not player text (debug tools are English only)
199        let help = help_line(&[
200            (Some(cursor_keys_name(km)), "move"),
201            (key(UiAction::Confirm), "choose"),
202            (key(UiAction::Cancel), "back"),
203        ]);
204        DebugMenuView {
205            // check-text: not player text (debug tools are English only)
206            title: "Debug tools".to_owned(),
207            menu: self.menu.view(),
208            help,
209        }
210    }
211}
212
213impl<G: GameData> Screen<G> for DebugMenuScreen<G> {
214    fn name(&self) -> &'static str {
215        Self::NAME
216    }
217
218    /// Its menu's.
219    fn actions(&self) -> Option<Vec<G::Action>> {
220        Some(crate::input::ui_actions(Menu::<()>::ACTIONS))
221    }
222
223    fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> Transition<G> {
224        for &action in &input.actions {
225            match self.menu.handle_with_sound(action, &mut ctx.audio) {
226                Some(MenuEvent::Cancelled) => return Transition::Pop,
227                Some(MenuEvent::Chosen(i)) => {
228                    let Some(entry) = self.entries.get_mut(i) else {
229                        continue;
230                    };
231                    let transition = entry.run(ctx);
232                    if !matches!(transition, Transition::None) {
233                        return transition;
234                    }
235                    // Its name may have changed (the look in use, say).
236                    self.menu = Self::menu_of(ctx, &self.entries).focused(i);
237                }
238                None => {}
239            }
240        }
241        Transition::None
242    }
243
244    fn draw(&self, ctx: &Ctx<G>, buf: &mut GlyphBuffer) {
245        glyph::paint(ctx, &self.view(ctx), buf);
246    }
247}
248
249/// What the glyph sampler adds under its palette: the game's sample
250/// panels from row `top` down, in its colors.
251pub type SamplePanels = fn(&mut GlyphBuffer, &Palette, i32);
252
253/// The [`glyph_sampler`](glyph::sampler) as a screen; Cancel closes it.
254#[derive(Debug, Clone)]
255pub struct GlyphSamplerScreen {
256    /// Drawn once on creation; the sampler never changes.
257    sampler: GlyphBuffer,
258}
259
260impl GlyphSamplerScreen {
261    /// Name reported by [`Screen::name`].
262    pub const NAME: &'static str = "glyph_sampler";
263
264    /// The sampler of `glyphs` (the font's), `ctx`'s palette and the game's
265    /// `panels`, on a console of `spec`.
266    pub fn new<G: GameData>(
267        ctx: &Ctx<G>,
268        spec: ConsoleSpec,
269        glyphs: &[char],
270        panels: SamplePanels,
271    ) -> Self {
272        Self {
273            sampler: glyph::sampler(spec, &ctx.palette, glyphs, panels),
274        }
275    }
276}
277
278impl<G: GameData> Screen<G> for GlyphSamplerScreen {
279    fn name(&self) -> &'static str {
280        Self::NAME
281    }
282
283    /// Cancel closes it.
284    fn actions(&self) -> Option<Vec<G::Action>> {
285        Some(crate::input::ui_actions(&[UiAction::Cancel]))
286    }
287
288    fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> Transition<G> {
289        if input
290            .actions
291            .iter()
292            .any(|a| a.ui() == Some(UiAction::Cancel))
293        {
294            ctx.audio.menu(MenuSound::Cancel);
295            Transition::Pop
296        } else {
297            Transition::None
298        }
299    }
300
301    fn draw(&self, _ctx: &Ctx<G>, buf: &mut GlyphBuffer) {
302        buf.blit(&self.sampler, 0, 0);
303    }
304}
305
306#[cfg(test)]
307mod tests;