Skip to main content

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}