Skip to main content

shuyi_ui/grid/
debug.rs

1//! The grid kit's channel of the debug view (ADR-0085): [`GRID`], the
2//! layout of a [`MapScene`] as its skin laid it out ([`Grid`]). A game's
3//! map screen offers it by calling [`layout_gizmos`] from its
4//! [`debug_draw`](crate::screen::Screen::debug_draw):
5//!
6//! ```ignore
7//! fn debug_draw(&self, _ctx: &Ctx<MyGame>, out: &mut Gizmos) {
8//!     let scene = self.scene();
9//!     let grid = self.skin.grid(&scene, AREA);
10//!     shuyi_ui::grid::debug::layout_gizmos(&scene, &grid, out);
11//! }
12//! ```
13
14use crate::debug::gizmo::{Channel, Gizmo, GizmoRole, Gizmos};
15use crate::frame::glyph_buffer::{PxRect, Rect};
16use crate::prelude::*;
17
18use super::layout::Grid;
19use super::scene::{MapScene, MapUnit};
20
21/// The grid kit's channel: a map's layout.
22pub const GRID: Channel = Channel::new("grid", "Grid layout");
23
24/// Hands in the [`GRID`] channel's gizmos for `scene` laid out as `grid`
25/// (its skin's [`GridSkin::grid`](super::GridSkin::grid)), in this order:
26///
27/// - the cell lines inside the view: each column's and row's first pixel,
28///   and the view's last ([`GizmoRole::Bounds`]);
29/// - the map's bounds, where they are on screen: round the tiles of the
30///   view that are on the map (`Bounds`);
31/// - the cursor's cell ([`GizmoRole::Note`]);
32/// - each unit's cell, and its index in `scene.units` as a label in the
33///   cell holding the tile's top-right pixel (`Note`);
34/// - each edge the map goes on past ([`MapScene::more`]): a line along that
35///   side of the view (`Note`).
36///
37/// Everything lies inside the view and the frame ([`Gizmos::frame`]); a
38/// cell the frame cuts is outlined where it is cut. Nothing is built while
39/// the channel is off.
40pub fn layout_gizmos<U: MapUnit>(scene: &MapScene<U>, grid: &Grid, out: &mut Gizmos) {
41    if !out.channel_on(GRID) {
42        return;
43    }
44    let Some(view) = grid.bounds().intersect(&out.frame()) else {
45        return;
46    };
47    let (left, top) = (view.x, view.y);
48    let (right, bottom) = (view.x + view.w - 1, view.y + view.h - 1);
49    let mut push = |role, gizmo| out.push(GRID, role, gizmo);
50    let (across, down) = (grid.tiles.0.max(0), grid.tiles.1.max(0));
51
52    let line = |from, to| Gizmo::Line { from, to };
53    let columns = (0..across).map(|dx| grid.rect_at(dx, 0).x);
54    for x in columns.filter(|&x| x >= left && x < right).chain([right]) {
55        push(GizmoRole::Bounds, line((x, top), (x, bottom)));
56    }
57    let rows = (0..down).map(|dy| grid.rect_at(0, dy).y);
58    for y in rows.filter(|&y| y >= top && y < bottom).chain([bottom]) {
59        push(GizmoRole::Bounds, line((left, y), (right, y)));
60    }
61
62    let on_map = (0..down)
63        .flat_map(|dy| (0..across).map(move |dx| (dx, dy)))
64        .filter(|&(dx, dy)| scene.tile_at(dx, dy).is_some_and(|t| t.terrain.is_some()))
65        .map(|(dx, dy)| grid.rect_at(dx, dy))
66        .reduce(union);
67    if let Some(rect) = on_map.and_then(|map| map.intersect(&view)) {
68        push(GizmoRole::Bounds, Gizmo::Rect { rect });
69    }
70
71    let cell = |pos| grid.rect(pos).and_then(|r| r.intersect(&view));
72    if let Some(rect) = scene.cursor_tile().and_then(cell) {
73        push(GizmoRole::Note, Gizmo::Rect { rect });
74    }
75    for (i, unit) in scene.units.iter().enumerate() {
76        let Some(rect) = cell(unit.pos()) else {
77            continue;
78        };
79        push(GizmoRole::Note, Gizmo::Rect { rect });
80        let at = (rect.x + rect.w - 1, rect.y);
81        push(
82            GizmoRole::Note,
83            Gizmo::Label {
84                at,
85                text: i.to_string(),
86            },
87        );
88    }
89
90    let more = scene.more;
91    let edges = [
92        (more.left(), (left, top), (left, bottom)),
93        (more.right(), (right, top), (right, bottom)),
94        (more.up(), (left, top), (right, top)),
95        (more.down(), (left, bottom), (right, bottom)),
96    ];
97    for (_, from, to) in edges.into_iter().filter(|e| e.0) {
98        push(GizmoRole::Note, line(from, to));
99    }
100}
101
102/// The smallest rectangle holding both.
103fn union(a: PxRect, b: PxRect) -> PxRect {
104    let (x0, y0) = (a.x.min(b.x), a.y.min(b.y));
105    let (x1, y1) = ((a.x + a.w).max(b.x + b.w), (a.y + a.h).max(b.y + b.h));
106    Rect::new(x0, y0, x1 - x0, y1 - y0)
107}
108
109#[cfg(test)]
110mod tests {
111    use proptest::prelude::*;
112    use shuyi_core::Pos;
113
114    use super::*;
115    use crate::debug::gizmo::Drawn;
116    use crate::frame::console::ConsoleSpec;
117    use crate::grid::camera::Camera;
118    use crate::grid::scene::{CursorStyle, CursorView, MapEdges};
119    use crate::grid::skin::GridSkin;
120    use crate::toy::grid::{GLYPH, Piece};
121
122    /// The toy's console, 40 × 20 cells of 8 × 16 pixels.
123    const CONSOLE: ConsoleSpec = ConsoleSpec::text(40, 20);
124
125    /// The scene of a `map` map seen through `area` with the camera as
126    /// near `origin` as the map lets it be, its terrain all grass, with
127    /// `units` and the cursor at `cursor`.
128    fn scene(
129        map: (u16, u16),
130        area: Rect,
131        origin: Pos,
132        units: &[Pos],
133        cursor: Pos,
134    ) -> MapScene<Piece> {
135        let view = GridSkin::<Piece>::view_tiles(&GLYPH, area);
136        let half = Pos::new(origin.x + view.0 / 2, origin.y + view.1 / 2);
137        let camera = Camera::centred_on(half, map.0, map.1, view);
138        let mut scene = MapScene::new(camera.origin, view);
139        let (w, h) = (i32::from(map.0), i32::from(map.1));
140        scene.set_terrain(|p| {
141            ((0..w).contains(&p.x) && (0..h).contains(&p.y)).then_some(shuyi_core::TerrainId(0))
142        });
143        scene.more = MapEdges::of_view(camera.origin, view, (w, h));
144        for (i, &pos) in (0..).zip(units) {
145            scene.push_unit(Piece::new(i, 'u', pos));
146        }
147        scene.cursor = Some(CursorView {
148            pos: cursor,
149            brightness: 1.0,
150            style: CursorStyle::Corners,
151        });
152        scene
153    }
154
155    /// The channel's gizmos, the channel on, in a frame of the console.
156    fn gizmos(scene: &MapScene<Piece>, area: Rect) -> Vec<Drawn> {
157        let grid = GridSkin::<Piece>::grid(&GLYPH, scene, area);
158        let frame = frame();
159        let mut out = Gizmos::with_on(["grid"]).within(frame);
160        layout_gizmos(scene, &grid, &mut out);
161        assert_eq!(out.offered(), [GRID]);
162        out.drawn().to_vec()
163    }
164
165    /// The edge lines: the `Note` lines.
166    fn edges(drawn: &[Drawn]) -> Vec<&Gizmo> {
167        drawn
168            .iter()
169            .filter(|d| d.role == GizmoRole::Note && matches!(d.gizmo, Gizmo::Line { .. }))
170            .map(|d| &d.gizmo)
171            .collect()
172    }
173
174    const AREA: Rect = Rect::new(0, 0, 40, 20);
175
176    /// The console's pixels.
177    fn frame() -> Rect {
178        let (w, h) = CONSOLE.px();
179        Rect::new(0, 0, w, h)
180    }
181
182    #[test]
183    fn nothing_is_built_while_the_channel_is_off() {
184        let s = scene((10, 8), AREA, Pos::new(0, 0), &[], Pos::new(0, 0));
185        let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
186        let mut off = Gizmos::new();
187        layout_gizmos(&s, &grid, &mut off);
188        assert!(off.is_empty());
189        assert_eq!(off.offered(), [GRID]);
190    }
191
192    #[test]
193    fn a_map_that_fits_the_view_has_no_edges_and_its_bounds_inside_it() {
194        // 10 × 8 tiles in a view of 20 × 20: centred, all of it shown.
195        let s = scene(
196            (10, 8),
197            AREA,
198            Pos::new(2, 3),
199            &[Pos::new(2, 3), Pos::new(3, 4)],
200            Pos::new(2, 3),
201        );
202        assert_eq!(s.more, MapEdges::NONE);
203        let drawn = gizmos(&s, AREA);
204        assert_eq!(edges(&drawn), [] as [&Gizmo; 0]);
205        let roles: Vec<_> = drawn.iter().map(|d| d.role).collect();
206        // 21 + 21 cell lines, the bounds, the cursor, two units and their
207        // labels, in that order.
208        assert_eq!(roles.len(), 21 + 21 + 1 + 1 + 4);
209        assert!(roles[..43].iter().all(|&r| r == GizmoRole::Bounds));
210        assert!(roles[43..].iter().all(|&r| r == GizmoRole::Note));
211        // The map's bounds: 10 × 8 tiles of 16 × 16 pixels, centred.
212        let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
213        let map = Rect::new(
214            grid.tile_px(Pos::new(0, 0)).0,
215            grid.tile_px(Pos::new(0, 0)).1,
216            160,
217            128,
218        );
219        assert_eq!(drawn[42].gizmo, Gizmo::Rect { rect: map });
220        let cursor = grid.rect(Pos::new(2, 3)).unwrap();
221        assert_eq!(drawn[43].gizmo, Gizmo::Rect { rect: cursor });
222        assert_eq!(drawn[44].gizmo, Gizmo::Rect { rect: cursor });
223        let label = Gizmo::Label {
224            at: (cursor.x + 15, cursor.y),
225            text: "0".to_owned(),
226        };
227        assert_eq!(drawn[45].gizmo, label);
228        assert!(matches!(&drawn[47].gizmo, Gizmo::Label { text, .. } if text == "1"));
229    }
230
231    #[test]
232    fn a_frame_that_cuts_the_view_on_a_cells_edge_draws_that_line_once() {
233        let s = scene((10, 8), AREA, Pos::new(0, 0), &[], Pos::new(0, 0));
234        let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
235        // Tiles of 16 × 16 from (0, 0); the frame's last pixel column is
236        // 32, a column's first, and its last row 48, a row's first.
237        let mut out = Gizmos::with_on(["grid"]).within(Rect::new(0, 0, 33, 49));
238        layout_gizmos(&s, &grid, &mut out);
239        let lines = |across: bool| {
240            out.drawn()
241                .iter()
242                .filter_map(|d| match d.gizmo {
243                    Gizmo::Line { from, to }
244                        if d.role == GizmoRole::Bounds && (from.1 == to.1) == across =>
245                    {
246                        Some(if across { from.1 } else { from.0 })
247                    }
248                    _ => None,
249                })
250                .collect::<Vec<_>>()
251        };
252        assert_eq!(lines(false), [0, 16, 32]);
253        assert_eq!(lines(true), [0, 16, 32, 48]);
254    }
255
256    #[test]
257    fn the_union_holds_both_rectangles_whichever_comes_first() {
258        let (a, b) = (Rect::new(10, 20, 5, 6), Rect::new(-3, 2, 4, 30));
259        let both = Rect::new(-3, 2, 18, 30);
260        assert_eq!(union(a, b), both);
261        assert_eq!(union(b, a), both);
262        assert_eq!(union(a, a), a);
263    }
264
265    #[test]
266    fn edges_are_drawn_only_on_the_sides_the_map_goes_on_past() {
267        // A 60 × 50 map, seen from its top-left corner: more right and down.
268        let s = scene((60, 50), AREA, Pos::new(0, 0), &[], Pos::new(0, 0));
269        assert_eq!(s.more.names(), ["right", "down"]);
270        let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
271        let view = grid.bounds();
272        let (r, b) = (view.x + view.w - 1, view.y + view.h - 1);
273        let drawn = gizmos(&s, AREA);
274        let right = Gizmo::Line {
275            from: (r, view.y),
276            to: (r, b),
277        };
278        let down = Gizmo::Line {
279            from: (view.x, b),
280            to: (r, b),
281        };
282        assert_eq!(edges(&drawn), [&right, &down]);
283        // From the middle: every side.
284        let middle = scene((60, 50), AREA, Pos::new(30, 25), &[], Pos::new(30, 25));
285        assert_eq!(middle.more.names(), ["left", "right", "up", "down"]);
286        assert_eq!(edges(&gizmos(&middle, AREA)).len(), 4);
287    }
288
289    /// Every pixel a gizmo names.
290    fn points(gizmo: &Gizmo) -> Vec<(i32, i32)> {
291        match gizmo {
292            Gizmo::Rect { rect } => {
293                vec![(rect.x, rect.y), (rect.x + rect.w - 1, rect.y + rect.h - 1)]
294            }
295            Gizmo::Line { from, to } => vec![*from, *to],
296            Gizmo::Label { at, .. } | Gizmo::Cross { at, .. } => vec![*at],
297            Gizmo::Circle { center, .. } => vec![*center],
298        }
299    }
300
301    proptest! {
302        #[test]
303        fn no_gizmo_leaves_the_frame(
304            map in (1_u16..80, 1_u16..80),
305            area in (-6..44i32, -4..24i32, 0..60i32, 0..30i32),
306            origin in (-20..90i32, -20..90i32),
307            units in prop::collection::vec((-5..85i32, -5..85i32), 0..5),
308            cursor in (-5..85i32, -5..85i32),
309        ) {
310            let area = Rect::new(area.0, area.1, area.2, area.3);
311            let units: Vec<_> = units.into_iter().map(|(x, y)| Pos::new(x, y)).collect();
312            let s = scene(map, area, Pos::new(origin.0, origin.1), &units, Pos::new(cursor.0, cursor.1));
313            let frame = frame();
314            for drawn in gizmos(&s, area) {
315                if let Gizmo::Rect { rect } = drawn.gizmo {
316                    prop_assert!(!rect.is_empty());
317                }
318                for (x, y) in points(&drawn.gizmo) {
319                    prop_assert!(frame.contains(x, y), "{:?} outside {frame:?}", drawn.gizmo);
320                }
321            }
322        }
323    }
324}