shuyi_ui/debug/gizmo.rs
1//! The debug view's data (ADR-0085): what a screen hands in to be drawn
2//! over the running game ([`Gizmo`]s, each in a [`Channel`] with a
3//! [`GizmoRole`]), one frame's list of them ([`Gizmos`]), and which
4//! channels are on ([`DebugView`], on [`Ctx::debug_view`]).
5//!
6//! A screen hands its gizmos in from
7//! [`Screen::debug_draw`](crate::screen::Screen::debug_draw), from `&self`
8//! and the context alone, so looking changes nothing. A channel's helper
9//! asks [`Gizmos::channel_on`] first and skips the work when it is off:
10//!
11//! ```
12//! use shuyi_ui::debug::gizmo::{Channel, Gizmo, GizmoRole, Gizmos};
13//! use shuyi_ui::frame::Rect;
14//!
15//! const BOXES: Channel = Channel::new("boxes", "Hit boxes");
16//!
17//! fn boxes(hits: &[Rect], out: &mut Gizmos) {
18//! if !out.channel_on(BOXES) {
19//! return;
20//! }
21//! for &rect in hits {
22//! out.push(BOXES, GizmoRole::Solid, Gizmo::Rect { rect });
23//! }
24//! }
25//!
26//! let mut out = Gizmos::with_on(["boxes"]);
27//! boxes(&[Rect::new(8, 16, 8, 8)], &mut out);
28//! assert_eq!(out.len(), 1);
29//! assert_eq!(out.offered(), [BOXES]);
30//! ```
31//!
32//! [`Ctx::debug_view`]: crate::Ctx::debug_view
33
34use alloc::collections::BTreeSet;
35
36use serde::{Deserialize, Serialize};
37
38use crate::frame::glyph_buffer::PxRect;
39use crate::prelude::*;
40
41pub use crate::frame::gizmo::GizmoRole;
42
43/// A shape the debug view draws over the frame, in console pixels, like
44/// the frame's items. Its colour comes from its role
45/// ([`GizmoRole::color`]), never from the screen.
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47#[non_exhaustive]
48pub enum Gizmo {
49 /// The outline of a rectangle, one pixel wide, inside it.
50 Rect {
51 /// The rectangle.
52 rect: PxRect,
53 },
54 /// A line one pixel wide, both ends drawn.
55 Line {
56 /// Its first pixel.
57 from: (i32, i32),
58 /// Its last pixel.
59 to: (i32, i32),
60 },
61 /// A plus, `size` pixels each way from its centre.
62 Cross {
63 /// Its centre.
64 at: (i32, i32),
65 /// How far each arm reaches.
66 size: u8,
67 },
68 /// The outline of a circle, `2 × radius + 1` pixels across.
69 Circle {
70 /// Its centre.
71 center: (i32, i32),
72 /// Its radius.
73 radius: u16,
74 },
75 /// Text, ASCII only (debug tools are English only), in the glyph
76 /// cells from the one holding pixel `at`, in the frame's font. Up to
77 /// [`LABEL_MAX`](crate::frame::gizmo::LABEL_MAX) characters are kept.
78 Label {
79 /// A pixel of its first cell.
80 at: (i32, i32),
81 /// The text.
82 text: String,
83 },
84}
85
86/// A kind of thing the debug view shows, turned on and off as one from
87/// the debug menu: a stable `snake_case` id (`grid`, `collision`), which
88/// is what is saved, and a label for the menu.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub struct Channel {
91 id: &'static str,
92 label: &'static str,
93}
94
95impl Channel {
96 /// The channel `id`, called `label` on the menu.
97 pub const fn new(id: &'static str, label: &'static str) -> Self {
98 Self { id, label }
99 }
100
101 /// Its id.
102 pub const fn id(self) -> &'static str {
103 self.id
104 }
105
106 /// Its name on the menu.
107 pub const fn label(self) -> &'static str {
108 self.label
109 }
110}
111
112/// A gizmo handed in, with its channel and role.
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
114#[non_exhaustive]
115pub struct Drawn {
116 /// Its channel.
117 pub channel: Channel,
118 /// What it shows.
119 pub role: GizmoRole,
120 /// Its shape.
121 pub gizmo: Gizmo,
122}
123
124/// What one frame hands in: the gizmos of the channels that are on, in
125/// the order they were pushed, and every channel offered, on or off. It
126/// knows the frame's pixels ([`frame`](Self::frame)), so a helper can keep
127/// its gizmos on it.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct Gizmos {
130 /// The frame's pixels.
131 frame: PxRect,
132 /// The ids of the channels that are on.
133 on: BTreeSet<String>,
134 /// Every channel is on, whatever its id.
135 all: bool,
136 /// The channels offered, in the order first offered.
137 offered: Vec<Channel>,
138 /// The gizmos kept.
139 drawn: Vec<Drawn>,
140}
141
142/// The frame of a list that wasn't told its own: about all of `i32`.
143const ANY_FRAME: PxRect = PxRect::new(-(1 << 30), -(1 << 30), i32::MAX, i32::MAX);
144
145impl Default for Gizmos {
146 fn default() -> Self {
147 Self {
148 frame: ANY_FRAME,
149 on: BTreeSet::new(),
150 all: false,
151 offered: Vec::new(),
152 drawn: Vec::new(),
153 }
154 }
155}
156
157impl Gizmos {
158 /// A frame's list with no channel on: it only gathers the channels
159 /// offered.
160 pub fn new() -> Self {
161 Self::default()
162 }
163
164 /// The same list for a frame of `pixels` (the buffer's
165 /// [`pixel_bounds`](crate::frame::GlyphBuffer::pixel_bounds)).
166 #[must_use]
167 pub fn within(self, pixels: PxRect) -> Self {
168 Self {
169 frame: pixels,
170 ..self
171 }
172 }
173
174 /// The frame's pixels: what a helper keeps its gizmos inside. Nearly
175 /// all of `i32` unless the list was told ([`within`](Self::within));
176 /// the game tells it the buffer's.
177 pub fn frame(&self) -> PxRect {
178 self.frame
179 }
180
181 /// A frame's list with the channels of these ids on.
182 pub fn with_on<'a>(ids: impl IntoIterator<Item = &'a str>) -> Self {
183 Self {
184 on: ids.into_iter().map(str::to_owned).collect(),
185 ..Self::default()
186 }
187 }
188
189 /// A frame's list with every channel on.
190 pub fn all_on() -> Self {
191 Self {
192 all: true,
193 ..Self::default()
194 }
195 }
196
197 /// Whether `channel` is on, which also offers it (the debug menu lists
198 /// it). A channel's helper asks first and does nothing more when it is
199 /// off.
200 pub fn channel_on(&mut self, channel: Channel) -> bool {
201 if !self.offered.contains(&channel) {
202 self.offered.push(channel);
203 }
204 self.all || self.on.contains(channel.id)
205 }
206
207 /// Adds `gizmo` of `channel`, showing `role`, after those already
208 /// pushed, if the channel is on; it is dropped if not. Either way the
209 /// channel is offered.
210 pub fn push(&mut self, channel: Channel, role: GizmoRole, gizmo: Gizmo) {
211 if self.channel_on(channel) {
212 self.drawn.push(Drawn {
213 channel,
214 role,
215 gizmo,
216 });
217 }
218 }
219
220 /// The gizmos kept, in the order pushed.
221 pub fn drawn(&self) -> &[Drawn] {
222 &self.drawn
223 }
224
225 /// How many gizmos were kept.
226 pub fn len(&self) -> usize {
227 self.drawn.len()
228 }
229
230 /// Whether none was kept.
231 pub fn is_empty(&self) -> bool {
232 self.drawn.is_empty()
233 }
234
235 /// Every channel offered, on or off, in the order first offered.
236 pub fn offered(&self) -> &[Channel] {
237 &self.offered
238 }
239}
240
241/// Which channels of the debug view are on, and which the running screens
242/// have offered ([`Ctx::debug_view`](crate::Ctx::debug_view)). None is on
243/// at first. Turned on from the debug menu, which saves the choice
244/// ([`Ctx::set_debug_channel`](crate::Ctx::set_debug_channel)); a shipped
245/// build never reads or writes it.
246#[derive(Debug, Clone, Default, PartialEq, Eq)]
247pub struct DebugView {
248 /// The ids of the channels that are on.
249 on: BTreeSet<String>,
250 /// Every channel is on, offered or not ([`show_all`](Self::show_all)).
251 all: bool,
252 /// The channels offered since the game started, in the order first
253 /// offered: kept when the screens that offer them are hidden (by the
254 /// debug menu itself).
255 offered: Vec<Channel>,
256}
257
258/// The saved form of a [`DebugView`]: the channels on, by id.
259#[derive(Serialize, Deserialize, Default)]
260#[serde(default, rename = "DebugView")]
261struct Saved {
262 on: Vec<String>,
263}
264
265impl DebugView {
266 /// Whether the channel `id` is on.
267 pub fn is_on(&self, id: &str) -> bool {
268 self.all || self.on.contains(id)
269 }
270
271 /// Whether any channel is on.
272 pub fn any_on(&self) -> bool {
273 self.all || !self.on.is_empty()
274 }
275
276 /// The ids of the channels on, in order (not those only
277 /// [`show_all`](Self::show_all) turned on).
278 pub fn on(&self) -> impl Iterator<Item = &str> {
279 self.on.iter().map(String::as_str)
280 }
281
282 /// Turns the channel `id` on or off. It need not be offered yet: a
283 /// screen may offer it later.
284 pub fn set(&mut self, id: &str, on: bool) {
285 if on {
286 self.on.insert(id.to_owned());
287 } else {
288 self.on.remove(id);
289 }
290 }
291
292 /// Turns every channel on, offered now or later (`frame-png --debug
293 /// all`). Not saved.
294 pub fn show_all(&mut self) {
295 self.all = true;
296 }
297
298 /// The channels the running screens have offered, in the order first
299 /// offered.
300 pub fn offered(&self) -> &[Channel] {
301 &self.offered
302 }
303
304 /// Keeps `channels` among those offered, after those already there.
305 pub fn offer(&mut self, channels: &[Channel]) {
306 for &channel in channels {
307 if !self.offered.contains(&channel) {
308 self.offered.push(channel);
309 }
310 }
311 }
312
313 /// A frame's empty list, with this view's channels on.
314 pub fn gizmos(&self) -> Gizmos {
315 Gizmos {
316 on: self.on.clone(),
317 all: self.all,
318 ..Gizmos::default()
319 }
320 }
321
322 /// The saved form: the ids of the channels on (RON).
323 pub fn to_ron(&self) -> String {
324 let saved = Saved {
325 on: self.on.iter().cloned().collect(),
326 };
327 ron::to_string(&saved).unwrap_or_default()
328 }
329
330 /// The channels on, read from the saved form; what was offered is
331 /// kept.
332 ///
333 /// # Errors
334 ///
335 /// Why the text can't be read.
336 // check-text: not player text (warnings for the log)
337 pub fn read_ron(&mut self, text: &str) -> Result<(), String> {
338 let saved: Saved = ron::from_str(text).map_err(|e| format!("unreadable: {e}"))?;
339 self.on = saved.on.into_iter().collect();
340 Ok(())
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::frame::glyph_buffer::Rect;
348
349 const A: Channel = Channel::new("a", "Channel A");
350 const B: Channel = Channel::new("b", "Channel B");
351
352 fn rect(x: i32) -> Gizmo {
353 Gizmo::Rect {
354 rect: Rect::new(x, 0, 1, 1),
355 }
356 }
357
358 #[test]
359 fn push_keeps_the_order_filled_and_skips_a_channel_that_is_off() {
360 let mut out = Gizmos::with_on(["a"]);
361 out.push(A, GizmoRole::Bounds, rect(0));
362 out.push(B, GizmoRole::Note, rect(1));
363 out.push(A, GizmoRole::Note, rect(2));
364 let kept: Vec<_> = out.drawn().iter().map(|d| (d.channel, d.role)).collect();
365 assert_eq!(kept, [(A, GizmoRole::Bounds), (A, GizmoRole::Note)]);
366 assert_eq!(out.drawn()[1].gizmo, rect(2));
367 assert_eq!(out.len(), 2);
368 // Both were offered, in the order first offered.
369 assert_eq!(out.offered(), [A, B]);
370 }
371
372 #[test]
373 fn channel_on_offers_the_channel_and_says_whether_it_is_on() {
374 let mut none = Gizmos::new();
375 assert!(!none.channel_on(B));
376 assert!(!none.channel_on(B));
377 assert!(none.is_empty());
378 assert_eq!(none.offered(), [B]);
379 // A list not told its frame takes nearly all of `i32`; told, it
380 // keeps the frame.
381 let reach = Rect::new(-(1 << 30), -(1 << 30), i32::MAX, i32::MAX);
382 assert_eq!(none.frame(), reach);
383 let frame = Rect::new(0, 0, 320, 240);
384 assert_eq!(Gizmos::new().within(frame).frame(), frame);
385 let mut all = Gizmos::all_on();
386 assert!(all.channel_on(A) && all.channel_on(B));
387 assert_eq!((A.id(), A.label()), ("a", "Channel A"));
388 }
389
390 #[test]
391 fn a_view_turns_channels_on_keeps_what_was_offered_and_saves_the_ones_on() {
392 let mut view = DebugView::default();
393 assert!(!view.any_on() && !view.is_on("a"));
394 view.set("b", true);
395 view.set("a", true);
396 view.set("a", false);
397 assert!(view.is_on("b") && !view.is_on("a") && view.any_on());
398 view.offer(&[B, A]);
399 view.offer(&[A]);
400 assert_eq!(view.offered(), [B, A]);
401 let mut gizmos = view.gizmos();
402 assert!(gizmos.channel_on(B) && !gizmos.channel_on(A));
403
404 let saved = view.to_ron();
405 assert_eq!(saved, r#"(on:["b"])"#);
406 let mut back = DebugView::default();
407 back.read_ron(&saved).unwrap();
408 assert_eq!(back.on().collect::<Vec<_>>(), ["b"]);
409 assert!(back.read_ron("(on: 3)").is_err());
410 // What is missing reads as nothing on.
411 back.read_ron("()").unwrap();
412 assert!(!back.any_on());
413
414 view.show_all();
415 assert!(view.is_on("anything") && view.gizmos().channel_on(A));
416 // Showing all isn't saved.
417 assert_eq!(view.to_ron(), r#"(on:["b"])"#);
418 }
419}