1use crate::{CellReader, GlyphCatalog, Pos, Window};
4
5#[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
37fn 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 #[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 #[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 #[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 #[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}