Skip to main content

monospace_core/
render.rs

1//! Turning cells into text. See _Rendering_ in [`docs/model.md`](../../../docs/model.md).
2
3use crate::{CellReader, GlyphCatalog, Pos, Window};
4
5/// Renders the rectangle of `window` from `source` to a string of exactly `window.size.height`
6/// lines, each exactly `window.size.width` glyphs wide and ending in `\n`, the last line included.
7/// A glyph may be more than one character, so the line is the same rectangle without being the
8/// same character count.
9///
10/// Each position asks its own cell for the text it renders to — see [`Cell::glyph_str`] — and
11/// falls back to a space when there is no cell, or the cell has none to give. A position inside
12/// the rendered rectangle that the reader holds no cell for renders as a space too, since it never
13/// held one — and so does a position that the rectangle's origin and an offset within its size
14/// cannot even address as a `Pos`, since nothing could ever have been stamped there.
15///
16/// **A reader rather than a buffer**: rendering asks for a cell and has no way to ask who owns
17/// one, or to reach anything else a [`Buffer`] keeps.
18///
19/// **The window is passed rather than taken from the reader**, and a reader need not have one: a
20/// rendering asks for the rectangle the cells were drawn in, a part of it, or one that reaches
21/// past it — the last of which is what the paragraph above is about.
22///
23/// [`Buffer`]: crate::Buffer
24/// [`Cell::glyph_str`]: crate::Cell::glyph_str
25#[must_use]
26pub fn render(source: &impl CellReader, glyphs: &GlyphCatalog, window: Window) -> String {
27    (0..window.size.height)
28        .map(|dy| {
29            let row: String = (0..window.size.width)
30                .map(|dx| glyph_at(source, glyphs, window.origin, dx, dy))
31                .collect();
32            row + "\n"
33        })
34        .collect()
35}
36
37/// The glyph at the absolute position `origin` plus `(dx, dy)`, or a space if that position
38/// cannot be addressed as a `Pos`, holds no cell, or the cell there has no text to give.
39fn glyph_at<'a>(
40    source: &'a impl CellReader,
41    glyphs: &'a GlyphCatalog,
42    origin: Pos,
43    dx: u16,
44    dy: u16,
45) -> &'a str {
46    origin
47        .x
48        .checked_add_unsigned(dx)
49        .zip(origin.y.checked_add_unsigned(dy))
50        .and_then(|(x, y)| source.cell(Pos { x, y }))
51        .and_then(|cell| cell.glyph_str(glyphs))
52        .unwrap_or(" ")
53}
54
55#[cfg(test)]
56mod tests {
57    use super::render;
58    use crate::{
59        Arm, Buffer, Cell, Glyph, GlyphCatalog, Pos, Size, StampMode, StampSink, Stroke,
60        StrokeCell, Window,
61    };
62
63    fn light() -> Stroke {
64        Stroke::from("light")
65    }
66
67    #[test]
68    fn a_single_junction_renders_as_a_cross() {
69        let mut buffer = Buffer::new(Window {
70            origin: Pos { x: 0, y: 0 },
71            size: Size {
72                width: 1,
73                height: 1,
74            },
75        });
76        buffer.stamp(
77            Pos { x: 0, y: 0 },
78            StrokeCell {
79                base: light(),
80                top: Arm::Set(light()),
81                right: Arm::Set(light()),
82                bottom: Arm::Set(light()),
83                left: Arm::Set(light()),
84            }
85            .into(),
86            StampMode::Above,
87            None,
88        );
89
90        let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
91
92        assert_eq!(text, "┼\n");
93    }
94
95    #[test]
96    fn a_box_renders_from_eight_stamps() {
97        let mut buffer = Buffer::new(Window {
98            origin: Pos { x: 0, y: 0 },
99            size: Size {
100                width: 4,
101                height: 3,
102            },
103        });
104        let corner = |top, right, bottom, left| -> Cell {
105            StrokeCell {
106                base: light(),
107                top,
108                right,
109                bottom,
110                left,
111            }
112            .into()
113        };
114
115        let set = || Arm::Set(light());
116
117        buffer.stamp(
118            Pos { x: 0, y: 0 },
119            corner(Arm::Closed, set(), set(), Arm::Closed),
120            StampMode::Above,
121            None,
122        );
123        buffer.stamp(
124            Pos { x: 1, y: 0 },
125            corner(Arm::Closed, set(), Arm::Closed, set()),
126            StampMode::Above,
127            None,
128        );
129        buffer.stamp(
130            Pos { x: 2, y: 0 },
131            corner(Arm::Closed, set(), Arm::Closed, set()),
132            StampMode::Above,
133            None,
134        );
135        buffer.stamp(
136            Pos { x: 3, y: 0 },
137            corner(Arm::Closed, Arm::Closed, set(), set()),
138            StampMode::Above,
139            None,
140        );
141        buffer.stamp(
142            Pos { x: 0, y: 1 },
143            corner(set(), Arm::Closed, set(), Arm::Closed),
144            StampMode::Above,
145            None,
146        );
147        buffer.stamp(
148            Pos { x: 3, y: 1 },
149            corner(set(), Arm::Closed, set(), Arm::Closed),
150            StampMode::Above,
151            None,
152        );
153        buffer.stamp(
154            Pos { x: 0, y: 2 },
155            corner(set(), set(), Arm::Closed, Arm::Closed),
156            StampMode::Above,
157            None,
158        );
159        buffer.stamp(
160            Pos { x: 1, y: 2 },
161            corner(Arm::Closed, set(), Arm::Closed, set()),
162            StampMode::Above,
163            None,
164        );
165        buffer.stamp(
166            Pos { x: 2, y: 2 },
167            corner(Arm::Closed, set(), Arm::Closed, set()),
168            StampMode::Above,
169            None,
170        );
171        buffer.stamp(
172            Pos { x: 3, y: 2 },
173            corner(set(), Arm::Closed, Arm::Closed, set()),
174            StampMode::Above,
175            None,
176        );
177
178        let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
179
180        assert_eq!(text, "┌──┐\n│  │\n└──┘\n");
181    }
182
183    /// The example named "An abstaining stamp": the top arm survives a second stamp that
184    /// overwrote everything else, so the cell renders as a T rather than a cross.
185    #[test]
186    fn an_abstaining_stamp_renders_the_surviving_arm() {
187        let mut buffer = Buffer::new(Window {
188            origin: Pos { x: 0, y: 0 },
189            size: Size {
190                width: 1,
191                height: 1,
192            },
193        });
194        buffer.stamp(
195            Pos { x: 0, y: 0 },
196            StrokeCell {
197                base: light(),
198                top: Arm::Set(light()),
199                right: Arm::Closed,
200                bottom: Arm::Closed,
201                left: Arm::Closed,
202            }
203            .into(),
204            StampMode::Above,
205            None,
206        );
207        buffer.stamp(
208            Pos { x: 0, y: 0 },
209            StrokeCell {
210                base: light(),
211                top: Arm::Unset,
212                right: Arm::Set(light()),
213                bottom: Arm::Closed,
214                left: Arm::Set(light()),
215            }
216            .into(),
217            StampMode::Above,
218            None,
219        );
220
221        let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
222
223        assert_eq!(text, "┴\n");
224    }
225
226    /// The example named "A key with no rule": the light-only catalog has no rule mentioning
227    /// `double`, so the whole output is a space.
228    #[test]
229    fn a_key_the_catalog_does_not_answer_renders_as_a_space() {
230        let mut buffer = Buffer::new(Window {
231            origin: Pos { x: 0, y: 0 },
232            size: Size {
233                width: 1,
234                height: 1,
235            },
236        });
237        buffer.stamp(
238            Pos { x: 0, y: 0 },
239            StrokeCell {
240                base: Stroke::from("double"),
241                top: Arm::Set(Stroke::from("double")),
242                right: Arm::Closed,
243                bottom: Arm::Closed,
244                left: Arm::Closed,
245            }
246            .into(),
247            StampMode::Above,
248            None,
249        );
250
251        let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
252
253        assert_eq!(text, " \n");
254    }
255
256    /// The example named "An undefined cell and a closed one": neither the light table's answer
257    /// to an all-`Closed` cell nor the absence of a cell exists in this slice, and both render as
258    /// a space for different reasons — rule 9 for the first, rule 8 for the second.
259    #[test]
260    fn an_undefined_cell_and_a_fully_closed_one_both_render_as_a_space() {
261        let mut buffer = Buffer::new(Window {
262            origin: Pos { x: 0, y: 0 },
263            size: Size {
264                width: 2,
265                height: 1,
266            },
267        });
268        buffer.stamp(
269            Pos { x: 0, y: 0 },
270            StrokeCell {
271                base: light(),
272                top: Arm::Closed,
273                right: Arm::Closed,
274                bottom: Arm::Closed,
275                left: Arm::Closed,
276            }
277            .into(),
278            StampMode::Above,
279            None,
280        );
281
282        let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
283
284        assert_eq!(text, "  \n");
285    }
286
287    #[test]
288    fn a_buffer_with_no_positions_renders_as_spaces() {
289        let mut buffer = Buffer::new(Window {
290            origin: Pos { x: 0, y: 0 },
291            size: Size {
292                width: 0,
293                height: 0,
294            },
295        });
296        buffer.stamp(
297            Pos { x: 0, y: 0 },
298            StrokeCell {
299                base: light(),
300                top: Arm::Set(light()),
301                right: Arm::Set(light()),
302                bottom: Arm::Set(light()),
303                left: Arm::Set(light()),
304            }
305            .into(),
306            StampMode::Above,
307            None,
308        );
309
310        let text = render(
311            &buffer,
312            &GlyphCatalog::light(),
313            Window {
314                origin: Pos { x: 0, y: 0 },
315                size: Size {
316                    width: 2,
317                    height: 1,
318                },
319            },
320        );
321
322        assert_eq!(text, "  \n");
323    }
324
325    #[test]
326    fn an_origin_near_i16_max_does_not_overflow() {
327        let origin = Pos {
328            x: i16::MAX - 1,
329            y: 0,
330        };
331        let size = Size {
332            width: 3,
333            height: 1,
334        };
335        let buffer = Buffer::new(Window { origin, size });
336
337        let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
338
339        assert_eq!(text, "   \n");
340    }
341
342    #[test]
343    fn rendering_outside_the_window_fills_it_with_spaces() {
344        let mut buffer = Buffer::new(Window {
345            origin: Pos { x: 0, y: 0 },
346            size: Size {
347                width: 2,
348                height: 1,
349            },
350        });
351        buffer.stamp(
352            Pos { x: 0, y: 0 },
353            StrokeCell {
354                base: light(),
355                top: Arm::Set(light()),
356                right: Arm::Closed,
357                bottom: Arm::Set(light()),
358                left: Arm::Closed,
359            }
360            .into(),
361            StampMode::Above,
362            None,
363        );
364
365        let text = render(
366            &buffer,
367            &GlyphCatalog::light(),
368            Window {
369                origin: Pos { x: -1, y: 0 },
370                size: Size {
371                    width: 4,
372                    height: 1,
373                },
374            },
375        );
376
377        assert_eq!(text, " │  \n");
378    }
379
380    /// The rule after _Rendering_'s numbered list: a literal renders as itself, with no key built
381    /// and no lookup — a catalog with no rule at all still answers it.
382    #[test]
383    fn a_literal_renders_as_itself_with_a_catalog_that_has_no_rule_for_it() {
384        let mut buffer = Buffer::new(Window {
385            origin: Pos { x: 0, y: 0 },
386            size: Size {
387                width: 1,
388                height: 1,
389            },
390        });
391        buffer.stamp(
392            Pos { x: 0, y: 0 },
393            Cell::Literal(Glyph::new("A").expect("\"A\" is one glyph")),
394            StampMode::Above,
395            None,
396        );
397
398        let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
399
400        assert_eq!(text, "A\n");
401    }
402}