1use crate::cell::Side;
5use crate::shape::fragment::border::Border;
6use crate::shape::fragment::corner::Corner;
7use crate::shape::fragment::fill::Fill;
8use crate::{Glyph, Pos, Shape, Size, Stroke, Surface};
9
10#[derive(Debug)]
17pub struct BoxShape {
18 pub at: Pos,
20 pub size: Size,
22 pub stroke: Stroke,
24 pub fill: Option<Glyph>,
26}
27
28impl Shape for BoxShape {
29 fn draw(&self, surface: &mut dyn Surface) {
30 let Size { width, height } = self.size;
31 if width < 2 || height < 2 {
32 return;
33 }
34 let (Some(x1), Some(y1)) = (
35 self.at.x.checked_add_unsigned(width - 1),
36 self.at.y.checked_add_unsigned(height - 1),
37 ) else {
38 return;
39 };
40 let x0 = self.at.x;
41 let y0 = self.at.y;
42 let stroke = || self.stroke.clone();
43 let closes_interior = self.fill.is_some();
44
45 Corner {
46 at: Pos { x: x0, y: y0 },
47 opens: (Side::Right, Side::Bottom),
48 stroke: stroke(),
49 }
50 .draw(surface);
51 Corner {
52 at: Pos { x: x1, y: y0 },
53 opens: (Side::Bottom, Side::Left),
54 stroke: stroke(),
55 }
56 .draw(surface);
57 Corner {
58 at: Pos { x: x1, y: y1 },
59 opens: (Side::Top, Side::Left),
60 stroke: stroke(),
61 }
62 .draw(surface);
63 Corner {
64 at: Pos { x: x0, y: y1 },
65 opens: (Side::Top, Side::Right),
66 stroke: stroke(),
67 }
68 .draw(surface);
69
70 if width > 2 {
71 Border {
72 from: Pos { x: x0 + 1, y: y0 },
73 len: width - 2,
74 side: Side::Top,
75 stroke: stroke(),
76 closes_interior,
77 }
78 .draw(surface);
79 Border {
80 from: Pos { x: x0 + 1, y: y1 },
81 len: width - 2,
82 side: Side::Bottom,
83 stroke: stroke(),
84 closes_interior,
85 }
86 .draw(surface);
87 }
88 if height > 2 {
89 Border {
90 from: Pos { x: x0, y: y0 + 1 },
91 len: height - 2,
92 side: Side::Left,
93 stroke: stroke(),
94 closes_interior,
95 }
96 .draw(surface);
97 Border {
98 from: Pos { x: x1, y: y0 + 1 },
99 len: height - 2,
100 side: Side::Right,
101 stroke: stroke(),
102 closes_interior,
103 }
104 .draw(surface);
105 }
106 if width > 2
107 && height > 2
108 && let Some(glyph) = &self.fill
109 {
110 Fill {
111 at: Pos {
112 x: x0 + 1,
113 y: y0 + 1,
114 },
115 size: Size {
116 width: width - 2,
117 height: height - 2,
118 },
119 glyph: glyph.clone(),
120 }
121 .draw(surface);
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::BoxShape;
129 use crate::shape::counting::CountingSurface;
130 use crate::{Buffer, GlyphCatalog, Window};
131 use crate::{FigureLayer, Glyph, Pos, Shape, Size, StampMode, Stroke, render};
132
133 fn light() -> Stroke {
134 Stroke::from("light")
135 }
136
137 #[test]
139 fn a_6x3_box_renders_its_border() {
140 let origin = Pos { x: 0, y: 0 };
141 let size = Size {
142 width: 6,
143 height: 3,
144 };
145 let mut buffer = Buffer::new(Window { origin, size });
146 BoxShape {
147 at: origin,
148 size,
149 stroke: light(),
150 fill: None,
151 }
152 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
153
154 let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
155
156 assert_eq!(text, "┌────┐\n│ │\n└────┘\n");
157 }
158
159 #[test]
162 fn a_6x3_box_filled_keeps_its_border_and_fills_only_its_interior() {
163 let origin = Pos { x: 0, y: 0 };
164 let size = Size {
165 width: 6,
166 height: 3,
167 };
168 let mut buffer = Buffer::new(Window { origin, size });
169 BoxShape {
170 at: origin,
171 size,
172 stroke: light(),
173 fill: Some(Glyph::new("░").expect("\"░\" is one glyph")),
174 }
175 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
176
177 let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
178
179 assert_eq!(text, "┌────┐\n│░░░░│\n└────┘\n");
180 }
181
182 #[test]
184 fn a_2x2_box_is_four_corners_and_nothing_else() {
185 let origin = Pos { x: 0, y: 0 };
186 let size = Size {
187 width: 2,
188 height: 2,
189 };
190 let mut buffer = Buffer::new(Window { origin, size });
191 BoxShape {
192 at: origin,
193 size,
194 stroke: light(),
195 fill: None,
196 }
197 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
198
199 let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
200
201 assert_eq!(text, "┌┐\n└┘\n");
202 }
203
204 #[test]
206 fn a_box_below_2_in_either_dimension_draws_nothing() {
207 let origin = Pos { x: 0, y: 0 };
208 let size = Size {
209 width: 2,
210 height: 2,
211 };
212
213 for degenerate in [
214 Size {
215 width: 1,
216 height: 2,
217 },
218 Size {
219 width: 2,
220 height: 1,
221 },
222 Size {
223 width: 0,
224 height: 2,
225 },
226 ] {
227 let mut buffer = Buffer::new(Window { origin, size });
228 BoxShape {
229 at: origin,
230 size: degenerate,
231 stroke: light(),
232 fill: None,
233 }
234 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
235
236 let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
237
238 assert_eq!(text, " \n \n", "size {degenerate:?}");
239 }
240 }
241
242 #[test]
244 fn no_box_above_writes_any_position_more_than_once() {
245 for (size, fill) in [
246 (
247 Size {
248 width: 6,
249 height: 3,
250 },
251 None,
252 ),
253 (
254 Size {
255 width: 6,
256 height: 3,
257 },
258 Some(Glyph::new("░").expect("\"░\" is one glyph")),
259 ),
260 (
261 Size {
262 width: 2,
263 height: 2,
264 },
265 None,
266 ),
267 ] {
268 let mut surface = CountingSurface::default();
269 BoxShape {
270 at: Pos { x: 0, y: 0 },
271 size,
272 stroke: light(),
273 fill,
274 }
275 .draw(&mut surface);
276
277 assert!(
278 surface.max_writes() <= 1,
279 "size {size:?} wrote a position more than once"
280 );
281 }
282 }
283
284 #[test]
288 fn two_unfilled_boxes_render_a_crossing_at_every_cell_their_borders_overlap() {
289 let origin = Pos { x: 0, y: 0 };
290 let size = Size {
291 width: 9,
292 height: 5,
293 };
294 let mut buffer = Buffer::new(Window { origin, size });
295
296 BoxShape {
297 at: Pos { x: 0, y: 0 },
298 size: Size {
299 width: 6,
300 height: 4,
301 },
302 stroke: light(),
303 fill: None,
304 }
305 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
306 BoxShape {
307 at: Pos { x: 4, y: 2 },
308 size: Size {
309 width: 6,
310 height: 4,
311 },
312 stroke: light(),
313 fill: None,
314 }
315 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
316
317 let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
318
319 assert_eq!(
320 text,
321 "┌────┐ \n│ │ \n│ ┌┼───\n└───┼┘ \n │ \n"
322 );
323 }
324
325 #[test]
328 fn a_filled_box_among_unfilled_ones_still_closes_its_interior() {
329 let origin = Pos { x: 0, y: 0 };
330 let size = Size {
331 width: 9,
332 height: 5,
333 };
334 let mut buffer = Buffer::new(Window { origin, size });
335
336 BoxShape {
337 at: Pos { x: 0, y: 0 },
338 size: Size {
339 width: 6,
340 height: 4,
341 },
342 stroke: light(),
343 fill: None,
344 }
345 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
346 BoxShape {
347 at: Pos { x: 4, y: 2 },
348 size: Size {
349 width: 6,
350 height: 4,
351 },
352 stroke: light(),
353 fill: Some(Glyph::new("░").expect("\"░\" is one glyph")),
354 }
355 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
356
357 let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
358
359 assert_eq!(
360 text,
361 "┌────┐ \n│ │ \n│ ┌┴───\n└───┤░░░░\n │░░░░\n"
362 );
363 }
364
365 #[test]
370 fn two_unfilled_boxes_overlapping_horizontally_merge_into_t_junctions() {
371 let origin = Pos { x: 0, y: 0 };
372 let size = Size {
373 width: 6,
374 height: 3,
375 };
376 let mut buffer = Buffer::new(Window { origin, size });
377
378 BoxShape {
379 at: Pos { x: 0, y: 0 },
380 size: Size {
381 width: 4,
382 height: 3,
383 },
384 stroke: light(),
385 fill: None,
386 }
387 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
388 BoxShape {
389 at: Pos { x: 2, y: 0 },
390 size: Size {
391 width: 4,
392 height: 3,
393 },
394 stroke: light(),
395 fill: None,
396 }
397 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
398
399 let text = render(&buffer, &GlyphCatalog::light(), buffer.window());
400
401 assert_eq!(text, "┌─┬┬─┐\n│ ││ │\n└─┴┴─┘\n");
402 }
403}