1pub 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
48pub 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 let keys = HelpKeys::keyboard(&ctx.keymap);
56 (ctx.debug_tools && bound).then(|| format!("{} debug", key_name(keys, debug)))
58}
59
60pub type Run<G> = Box<dyn FnMut(&mut Ctx<G>) -> Transition<G>>;
63
64pub type Label<G> = Box<dyn Fn(&Ctx<G>) -> String>;
66
67pub struct DebugEntry<G: GameData> {
70 label: Label<G>,
72 run: Run<G>,
74}
75
76impl<G: GameData> DebugEntry<G> {
77 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 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 pub fn label(&self, ctx: &Ctx<G>) -> String {
99 (self.label)(ctx)
100 }
101
102 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#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct DebugMenuView {
117 pub title: String,
119 pub menu: MenuView,
121 pub help: String,
123}
124
125pub struct DebugMenuScreen<G: GameData> {
132 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 pub const NAME: &'static str = "debug_menu";
149
150 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 entries.push(DebugEntry::new("Save session", |ctx| {
158 ctx.ask_session_save();
159 Transition::None
160 }));
161 }
162 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 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 pub fn menu(&self) -> &Menu<usize> {
183 &self.menu
184 }
185
186 #[must_use]
188 pub fn focused(mut self, index: usize) -> Self {
189 self.menu = self.menu.focused(index);
190 self
191 }
192
193 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 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 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 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 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
249pub type SamplePanels = fn(&mut GlyphBuffer, &Palette, i32);
252
253#[derive(Debug, Clone)]
255pub struct GlyphSamplerScreen {
256 sampler: GlyphBuffer,
258}
259
260impl GlyphSamplerScreen {
261 pub const NAME: &'static str = "glyph_sampler";
263
264 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 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;