Skip to main content

shuyi_ui/debug/
glyph.rs

1//! The glyph look of the debug screens: the debug menu ([`paint`]), the
2//! glyph sampler ([`sampler`]), the scene camera ([`paint_camera`]), the
3//! Debug key's hint ([`paint_hint`]), the debug view's screen
4//! ([`paint_view`]) and its gizmos over the frame ([`paint_gizmos`]).
5
6use alloc::rc::Rc;
7
8use super::camera::SceneCameraView;
9use super::gizmo::{Gizmo, Gizmos};
10use super::view::DebugViewView;
11use super::{DebugMenuView, SamplePanels};
12use crate::ctx::{Ctx, GameData};
13use crate::frame::color::{EngineColor, Palette};
14use crate::frame::console::ConsoleSpec;
15use crate::frame::gizmo::{GizmoItem, GizmoShape, LabelText};
16use crate::frame::glyph_buffer::{BoxStyle, Cell, GlyphBuffer, Rect};
17use crate::prelude::*;
18use crate::screens::{centre_x, print_centred};
19use crate::widgets::menu;
20
21/// Row of the debug menu's title.
22const MENU_TITLE_ROW: i32 = 9;
23
24/// Paints the debug menu's `view` over the whole of `buf`: the title, the
25/// menu centred under it and the help line at the bottom.
26pub fn paint<G: GameData>(ctx: &Ctx<G>, view: &DebugMenuView, buf: &mut GlyphBuffer) {
27    let c = |e| ctx.palette.engine(e);
28    let black = c(EngineColor::Background);
29    buf.fill_rect(buf.bounds(), Cell::new(' ', c(EngineColor::Text), black));
30    let hi = c(EngineColor::Highlight);
31    print_centred(buf, MENU_TITLE_ROW, &view.title, hi, black);
32    let (w, _) = menu::glyph::size(ctx, &view.menu);
33    let x = centre_x(buf, usize::try_from(w).unwrap_or(0));
34    menu::glyph::paint(ctx, &view.menu, buf, x, MENU_TITLE_ROW + 2);
35    let bottom = i32::from(buf.height()) - 1;
36    print_centred(buf, bottom, &view.help, c(EngineColor::TextDim), black);
37}
38
39/// Paints the "Debug view" screen's `view` over the whole of `buf`, as
40/// the debug menu is painted: the title, the channels centred under it (or
41/// the line saying there are none) and the help line at the bottom.
42pub fn paint_view<G: GameData>(ctx: &Ctx<G>, view: &DebugViewView, buf: &mut GlyphBuffer) {
43    let c = |e| ctx.palette.engine(e);
44    let black = c(EngineColor::Background);
45    buf.fill_rect(buf.bounds(), Cell::new(' ', c(EngineColor::Text), black));
46    let hi = c(EngineColor::Highlight);
47    print_centred(buf, MENU_TITLE_ROW, &view.title, hi, black);
48    if let Some(empty) = &view.empty {
49        print_centred(
50            buf,
51            MENU_TITLE_ROW + 2,
52            empty,
53            c(EngineColor::TextDim),
54            black,
55        );
56    } else {
57        let (w, _) = menu::glyph::size(ctx, &view.menu);
58        let x = centre_x(buf, usize::try_from(w).unwrap_or(0));
59        menu::glyph::paint(ctx, &view.menu, buf, x, MENU_TITLE_ROW + 2);
60    }
61    let bottom = i32::from(buf.height()) - 1;
62    print_centred(buf, bottom, &view.help, c(EngineColor::TextDim), black);
63}
64
65/// Adds the debug view's `gizmos` to `buf` (ADR-0085), in the order
66/// handed in, over everything drawn so far: each as an
67/// [`Item::Gizmo`](crate::frame::Item::Gizmo) in its role's engine colour
68/// from `palette` ([`GizmoRole::color`](super::GizmoRole::color)), never
69/// the game's own. A label prints its text into the cells from the one
70/// holding its pixel, keeping their backgrounds, and its item's clip is
71/// those cells.
72pub fn paint_gizmos(palette: &Palette, gizmos: &Gizmos, buf: &mut GlyphBuffer) {
73    let (cell_w, cell_h) = (
74        i32::from(buf.spec().cell_w).max(1),
75        i32::from(buf.spec().cell_h).max(1),
76    );
77    for drawn in gizmos.drawn() {
78        let color = palette.engine(drawn.role.color());
79        let shape = match &drawn.gizmo {
80            Gizmo::Rect { rect } => GizmoShape::Rect { rect: *rect },
81            Gizmo::Line { from, to } => GizmoShape::Line {
82                from: *from,
83                to: *to,
84            },
85            Gizmo::Cross { at, size } => GizmoShape::Cross {
86                at: *at,
87                size: *size,
88            },
89            Gizmo::Circle { center, radius } => GizmoShape::Circle {
90                center: *center,
91                radius: *radius,
92            },
93            Gizmo::Label { at, text } => GizmoShape::Label {
94                at: *at,
95                text: LabelText::new(text),
96            },
97        };
98        let mut item = GizmoItem::new(drawn.channel.id(), drawn.role, shape, color);
99        if let GizmoShape::Label { at, text } = shape {
100            let (cx, cy) = (at.0.div_euclid(cell_w), at.1.div_euclid(cell_h));
101            let len = buf.text_cells(text.as_str());
102            buf.print_fg(cx, cy, text.as_str(), color);
103            item.clip = Rect::new(
104                cx.saturating_mul(cell_w),
105                cy.saturating_mul(cell_h),
106                len.saturating_mul(cell_w),
107                cell_h,
108            );
109        }
110        buf.add_gizmo(item);
111    }
112}
113
114/// Left column of the scene camera's title and the lines under it.
115const CAMERA_LEFT: i32 = 2;
116
117/// Paints the scene camera's `view` over the whole of `buf`: the title,
118/// the zoom and the help line; the picture through the window in a frame,
119/// the caption over it and the text box over its corner.
120pub fn paint_camera(palette: &Palette, view: &SceneCameraView, buf: &mut GlyphBuffer) {
121    let c = |e| palette.engine(e);
122    let (black, text, hi) = (
123        c(EngineColor::Background),
124        c(EngineColor::Text),
125        c(EngineColor::Highlight),
126    );
127    buf.fill_rect(buf.bounds(), Cell::new(' ', text, black));
128    buf.print(CAMERA_LEFT, 0, &view.title, hi, black);
129    let bottom = i32::from(buf.height()) - 1;
130    let dim = c(EngineColor::TextDim);
131    buf.print(CAMERA_LEFT, bottom, &view.help, dim, black);
132    buf.print(CAMERA_LEFT, 2, &view.info, text, black);
133    let Some((picture, frame)) = &view.shown else {
134        return;
135    };
136
137    let window = view.layout.window;
138    let border = Rect::new(window.x - 1, window.y - 1, window.w + 2, window.h + 2);
139    buf.draw_box(border, BoxStyle::Single, c(EngineColor::Border), black);
140    buf.fill_rect(window, Cell::see_through(' ', text));
141    buf.set_backdrop(Rc::clone(picture), window, frame.origin_px, frame.zoom);
142    // Glyphs over the scene, with no box behind them.
143    buf.print_fg(window.x + 1, window.y, &view.caption, hi);
144
145    let text_box = view.layout.text_box;
146    let panel = c(EngineColor::Panel);
147    buf.fill_rect(text_box, Cell::new(' ', text, panel));
148    buf.draw_box(text_box, BoxStyle::Double, c(EngineColor::Focus), panel);
149    for (row, line) in (text_box.y + 1..).zip(&view.box_text) {
150        buf.print(text_box.x + 2, row, line, text, panel);
151    }
152}
153
154/// Prints `hint` (a screen's view holds the [`hint`](super::hint))
155/// right-aligned on row `y`, one cell in from the edge, unless those cells
156/// or the one before them already hold text.
157pub fn paint_hint(palette: &Palette, hint: &str, buf: &mut GlyphBuffer, y: i32) {
158    let len = buf.text_cells(hint);
159    let x = i32::from(buf.width()) - 1 - len;
160    let free = (x - 1..x + len).all(|cx| buf.get(cx, y).is_some_and(|c| c.glyph == ' '));
161    if free {
162        buf.print(
163            x,
164            y,
165            hint,
166            palette.engine(EngineColor::TextDim),
167            palette.engine(EngineColor::Background),
168        );
169    }
170}
171
172/// Glyphs per sampler row; each glyph is followed by a blank cell.
173pub const GLYPHS_PER_ROW: usize = 48;
174/// Width of one palette swatch column (`██ name`).
175pub const SWATCH_W: i32 = 19;
176/// Longest colour name shown in a swatch column; longer names are cut.
177const SWATCH_NAME_W: usize = SWATCH_W as usize - 5;
178/// Swatch columns across the console.
179pub const SWATCH_COLUMNS: i32 = 5;
180
181/// Row of the "Palette" heading: right after the glyph grid's own title and
182/// rows, no blank row between. Computed from `glyphs` (rather than a fixed
183/// constant) so the bottom half doesn't waste rows the top half didn't use,
184/// however many glyphs the font ends up with.
185pub fn palette_top(glyphs: &[char]) -> i32 {
186    let glyph_rows: i32 = glyphs
187        .chunks(GLYPHS_PER_ROW)
188        .count()
189        .try_into()
190        .unwrap_or(i32::MAX);
191    1 + glyph_rows
192}
193
194/// Row where the sample panels start, for `palette`'s size and `glyphs`'
195/// row count: the palette title, its swatch rows, then the sample sentence,
196/// with no blank rows between (every row here is scarce once the palette is
197/// large).
198pub fn panels_top(palette: &Palette, glyphs: &[char]) -> i32 {
199    panels_top_for(palette.iter().count(), glyphs)
200}
201
202/// [`panels_top`] for a palette of `colours` colours.
203pub fn panels_top_for(colours: usize, glyphs: &[char]) -> i32 {
204    let swatch_rows: i32 = colours
205        .div_ceil(SWATCH_COLUMNS as usize)
206        .try_into()
207        .unwrap_or(i32::MAX);
208    palette_top(glyphs) + 2 + swatch_rows
209}
210
211/// A buffer of a console of `spec`: the top half shows every glyph in
212/// `glyphs` in a grid; the bottom half shows every palette colour as a
213/// swatch plus its name, a line of sample text and the game's `panels`.
214pub fn sampler(
215    spec: ConsoleSpec,
216    palette: &Palette,
217    glyphs: &[char],
218    panels: SamplePanels,
219) -> GlyphBuffer {
220    let c = |e| palette.engine(e);
221    let (text, dim, black) = (
222        c(EngineColor::Text),
223        c(EngineColor::TextDim),
224        c(EngineColor::Background),
225    );
226    let mut b = GlyphBuffer::with_spec(spec, Cell::new(' ', text, black));
227
228    // check-text: not player text (debug tools are English only)
229    let title = format!("Glyphs ({})", glyphs.len());
230    b.print(1, 0, &title, c(EngineColor::Highlight), black);
231    for (row, chunk) in (1..).zip(glyphs.chunks(GLYPHS_PER_ROW)) {
232        for (col, &g) in (0..).zip(chunk) {
233            b.print(2 + col * 2, row, &g.to_string(), text, black);
234        }
235    }
236
237    let bottom = palette_top(glyphs);
238    let colors: Vec<_> = palette.iter().collect();
239    // check-text: not player text (debug tools are English only)
240    let title = format!("Palette ({})", colors.len());
241    b.print(1, bottom, &title, c(EngineColor::Highlight), black);
242    for (i, &(name, rgb)) in (0..).zip(&colors) {
243        let (x, y) = (
244            1 + i % SWATCH_COLUMNS * SWATCH_W,
245            bottom + 1 + i / SWATCH_COLUMNS,
246        );
247        b.print(x, y, "██", rgb, black);
248        let name: String = name.chars().take(SWATCH_NAME_W).collect();
249        b.print(x + 3, y, &name, dim, black);
250    }
251
252    let panels_top = panels_top(palette, glyphs);
253    b.print(
254        1,
255        panels_top - 1,
256        // check-text: not player text (debug tools are English only)
257        "The quick brown fox jumps over the lazy dog. 0123456789 ÀÉÎÕÜ àéîõü ß ¿¡ «» ← ↑ → ↓",
258        text,
259        black,
260    );
261    panels(&mut b, palette, panels_top);
262    b
263}