Skip to main content

monospace_core/shape/
box_shape.rs

1//! The box: corners, border runs and an interior, placed correctly. See _The initial set_ in
2//! [`docs/model.md`](../../../docs/model.md).
3
4use 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/// A box: a position, a size and a stroke, with a fill as an option.
11///
12/// A **complete** shape in the sense of _Complete and fragment_ in
13/// [`docs/model.md`](../../../docs/model.md): what a library user sees. Its decomposition depends
14/// on `size` rather than on its kind — a 2×2 box is four corners and nothing else — and below 2
15/// in either dimension it draws nothing at all, per _Degenerate arrangements_.
16#[derive(Debug)]
17pub struct BoxShape {
18    /// The box's top-left corner.
19    pub at: Pos,
20    /// The box's width and height, in cells.
21    pub size: Size,
22    /// The stroke every cell this box writes is drawn in.
23    pub stroke: Stroke,
24    /// The glyph that fills the interior, or `None` for no fill at all.
25    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    /// A 6×3 box renders its border exactly.
138    #[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    /// The same box filled keeps its border and fills exactly its 4×1
160    /// interior.
161    #[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    /// A 2×2 box is four corners and nothing else.
183    #[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    /// Below 2 in either dimension, nothing is drawn at all.
205    #[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    /// None of the boxes above writes any position more than once.
243    #[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    /// Two unfilled boxes positioned so one's border
285    /// crosses into the other's interior render a crossing (`┼`) at every cell where that
286    /// happens, not a closed junction.
287    #[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    /// A fill keeps closing its own interior side, even
326    /// while an unfilled box elsewhere still lets a crossing show through.
327    #[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    /// Two unfilled boxes at the same row, overlapping horizontally by two columns, merge cleanly
366    /// into T-junctions at both cells where one's corner lands on the other's border — an
367    /// unfilled shape's interior side stays open to whatever else needs that cell, here as much
368    /// as at the diagonal overlap the bug was reported against.
369    #[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}