Skip to main content

monospace_core/shape/
line.rs

1//! The line: a run of one stroke, its two outermost cells carrying only the arm it runs on. See
2//! _The initial set_ and _What a terminal writes_ in
3//! [`docs/model.md`](../../../docs/model.md).
4
5use crate::cell::Side;
6use crate::shape::fragment::end::End;
7use crate::shape::fragment::segment::Segment;
8use crate::{Orientation, Pos, Shape, Stroke, Surface};
9
10/// A line: a position, a length, an orientation and a stroke. It names no glyph of its own — an
11/// end is one arm in the line's stroke, not a chosen glyph, because an end has to say where the
12/// stroke stops and what may join it there, which an arm does and a literal cannot: a literal
13/// composes as a cell with four `Closed` arms, so nothing connects into it and two lines meeting at
14/// a shared end could never make the corner they make.
15///
16/// A **complete** shape in the sense of _Complete and fragment_ in
17/// [`docs/model.md`](../../../docs/model.md). No minimum length is declared: a length of 0 draws
18/// nothing, a length of 1 draws its one position as an end, and neither is rejected.
19#[derive(Debug)]
20pub struct Line {
21    /// The position of the line's first cell.
22    pub at: Pos,
23    /// How many cells the line occupies. Any value is accepted, including 0.
24    pub len: u16,
25    /// Whether the line runs along a row or a column.
26    pub orientation: Orientation,
27    /// The stroke every cell this line writes is drawn in.
28    pub stroke: Stroke,
29}
30
31impl Line {
32    /// The position `i` cells along this line's orientation from `at`, or `None` if that would
33    /// overflow `i16`.
34    fn pos_at(&self, i: u16) -> Option<Pos> {
35        match self.orientation {
36            Orientation::Horizontal => self
37                .at
38                .x
39                .checked_add_unsigned(i)
40                .map(|x| Pos { x, y: self.at.y }),
41            Orientation::Vertical => self
42                .at
43                .y
44                .checked_add_unsigned(i)
45                .map(|y| Pos { x: self.at.x, y }),
46        }
47    }
48}
49
50impl Shape for Line {
51    fn draw(&self, surface: &mut dyn Surface) {
52        if self.len == 0 {
53            return;
54        }
55        let (forward, backward) = match self.orientation {
56            Orientation::Horizontal => (Side::Right, Side::Left),
57            Orientation::Vertical => (Side::Bottom, Side::Top),
58        };
59
60        if let Some(first) = self.pos_at(0) {
61            End {
62                at: first,
63                side: forward,
64                stroke: self.stroke.clone(),
65            }
66            .draw(surface);
67        }
68        if self.len >= 3
69            && let Some(from) = self.pos_at(1)
70        {
71            Segment {
72                from,
73                len: self.len - 2,
74                orientation: self.orientation,
75                stroke: self.stroke.clone(),
76            }
77            .draw(surface);
78        }
79        if self.len >= 2
80            && let Some(last) = self.pos_at(self.len - 1)
81        {
82            End {
83                at: last,
84                side: backward,
85                stroke: self.stroke.clone(),
86            }
87            .draw(surface);
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::Line;
95    use crate::shape::counting::CountingSurface;
96    use crate::{
97        Arm, Buffer, CellReader, FigureLayer, GlyphCatalog, Orientation, Pos, Shape, Size,
98        StampMode, Stroke, Window, render,
99    };
100
101    fn light() -> Stroke {
102        Stroke::from("light")
103    }
104
105    /// A horizontal line of length 5 renders `─────`, and the position
106    /// one past its end holds no cell at all.
107    #[test]
108    fn a_horizontal_line_of_length_5_renders_and_stops_cleanly() {
109        let origin = Pos { x: 0, y: 0 };
110        let size = Size {
111            width: 6,
112            height: 1,
113        };
114        let mut buffer = Buffer::new(Window { origin, size });
115        Line {
116            at: origin,
117            len: 5,
118            orientation: Orientation::Horizontal,
119            stroke: light(),
120        }
121        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
122
123        assert_eq!(
124            render(&buffer, &GlyphCatalog::light(), buffer.window()),
125            "───── \n"
126        );
127        assert!(buffer.cell(Pos { x: 5, y: 0 }).is_none());
128    }
129
130    /// A vertical line of length 4 renders four `│`s, and the position
131    /// one past its end holds no cell.
132    #[test]
133    fn a_vertical_line_of_length_4_renders_and_stops_cleanly() {
134        let origin = Pos { x: 0, y: 0 };
135        let size = Size {
136            width: 1,
137            height: 5,
138        };
139        let mut buffer = Buffer::new(Window { origin, size });
140        Line {
141            at: origin,
142            len: 4,
143            orientation: Orientation::Vertical,
144            stroke: light(),
145        }
146        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
147
148        assert_eq!(
149            render(&buffer, &GlyphCatalog::light(), buffer.window()),
150            "│\n│\n│\n│\n \n"
151        );
152        assert!(buffer.cell(Pos { x: 0, y: 4 }).is_none());
153    }
154
155    /// A horizontal line's first cell has its right arm `Set` and the
156    /// other three `Unset` — it decides nothing about what lies beyond it. `Cell` is public for
157    /// exactly this: a test that read the buffer back through `render` would depend on the catalog
158    /// and on the lookup, so a wrong glyph would fail a test named after composition.
159    #[test]
160    fn a_horizontal_lines_first_cell_carries_only_its_forward_arm() {
161        let origin = Pos { x: 0, y: 0 };
162        let mut buffer = Buffer::new(Window {
163            origin,
164            size: Size {
165                width: 5,
166                height: 1,
167            },
168        });
169        Line {
170            at: origin,
171            len: 5,
172            orientation: Orientation::Horizontal,
173            stroke: light(),
174        }
175        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
176
177        let Some(crate::Cell::Strokes(cell)) = buffer.cell(origin) else {
178            panic!("expected a stroke cell at the line's first position");
179        };
180        assert_eq!(cell.right, Arm::Set(Stroke::from("light")));
181        assert_eq!(cell.top, Arm::Unset);
182        assert_eq!(cell.bottom, Arm::Unset);
183        assert_eq!(cell.left, Arm::Unset);
184    }
185
186    /// A line of length 2 is two ends side by side, with no interior
187    /// run between them.
188    #[test]
189    fn a_line_of_length_2_is_two_ends_with_no_run_between_them() {
190        let origin = Pos { x: 0, y: 0 };
191        let size = Size {
192            width: 2,
193            height: 1,
194        };
195        let mut buffer = Buffer::new(Window { origin, size });
196        Line {
197            at: origin,
198            len: 2,
199            orientation: Orientation::Horizontal,
200            stroke: light(),
201        }
202        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
203
204        assert_eq!(
205            render(&buffer, &GlyphCatalog::light(), buffer.window()),
206            "──\n"
207        );
208    }
209
210    /// A line of length 1 returns normally and writes exactly one
211    /// position.
212    #[test]
213    fn a_line_of_length_1_writes_exactly_one_position() {
214        let origin = Pos { x: 0, y: 0 };
215        let mut buffer = Buffer::new(Window {
216            origin,
217            size: Size {
218                width: 1,
219                height: 1,
220            },
221        });
222        Line {
223            at: origin,
224            len: 1,
225            orientation: Orientation::Horizontal,
226            stroke: light(),
227        }
228        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
229
230        assert!(buffer.cell(origin).is_some());
231    }
232
233    /// A line of length 0 draws nothing and returns normally.
234    #[test]
235    fn a_line_of_length_0_draws_nothing() {
236        let origin = Pos { x: 0, y: 0 };
237        let mut buffer = Buffer::new(Window {
238            origin,
239            size: Size {
240                width: 1,
241                height: 1,
242            },
243        });
244        Line {
245            at: origin,
246            len: 0,
247            orientation: Orientation::Horizontal,
248            stroke: light(),
249        }
250        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
251
252        assert!(buffer.cell(origin).is_none());
253    }
254
255    /// No line above writes any position more than once, the
256    /// length-1 line included.
257    #[test]
258    fn no_line_above_writes_any_position_more_than_once() {
259        for len in [0, 1, 2, 3, 5] {
260            let mut surface = CountingSurface::default();
261            Line {
262                at: Pos { x: 0, y: 0 },
263                len,
264                orientation: Orientation::Horizontal,
265                stroke: light(),
266            }
267            .draw(&mut surface);
268
269            assert!(
270                surface.max_writes() <= 1,
271                "length {len} wrote a position more than once"
272            );
273        }
274    }
275
276    /// A horizontal line and a vertical line whose ends land on
277    /// `(0, 0)` render `┌` there — the corner the two make, not a segment, a T or a cross — in
278    /// both drawing orders and under both stamp modes. This is the scenario a caller-supplied end
279    /// glyph could not satisfy: a literal refuses every junction, so two lines meeting at a shared
280    /// end would have nothing to connect.
281    #[test]
282    fn two_lines_sharing_an_end_compose_into_the_corner_they_make() {
283        let origin = Pos { x: 0, y: 0 };
284        let size = Size {
285            width: 1,
286            height: 1,
287        };
288        let horizontal = || Line {
289            at: origin,
290            len: 3,
291            orientation: Orientation::Horizontal,
292            stroke: light(),
293        };
294        let vertical = || Line {
295            at: origin,
296            len: 3,
297            orientation: Orientation::Vertical,
298            stroke: light(),
299        };
300
301        for mode in [StampMode::Above, StampMode::Below] {
302            let mut horizontal_first = Buffer::new(Window { origin, size });
303            horizontal().draw(&mut FigureLayer::new(
304                &mut horizontal_first,
305                StampMode::Above,
306            ));
307            vertical().draw(&mut FigureLayer::new(&mut horizontal_first, mode));
308
309            let mut vertical_first = Buffer::new(Window { origin, size });
310            vertical().draw(&mut FigureLayer::new(&mut vertical_first, StampMode::Above));
311            horizontal().draw(&mut FigureLayer::new(&mut vertical_first, mode));
312
313            for (label, buffer) in [
314                ("horizontal first", &horizontal_first),
315                ("vertical first", &vertical_first),
316            ] {
317                assert_eq!(
318                    render(buffer, &GlyphCatalog::light(), buffer.window()),
319                    "┌\n",
320                    "{label}, second stamp under {mode:?}"
321                );
322            }
323        }
324    }
325}