monospace-core 0.1.0

Core library for Monospace: parsing, layout and ASCII rendering of diagrams
Documentation
//! The line: a run of one stroke, its two outermost cells carrying only the arm it runs on. See
//! _The initial set_ and _What a terminal writes_ in
//! [`docs/model.md`](../../../docs/model.md).

use crate::cell::Side;
use crate::shape::fragment::end::End;
use crate::shape::fragment::segment::Segment;
use crate::{Orientation, Pos, Shape, Stroke, Surface};

/// A line: a position, a length, an orientation and a stroke. It names no glyph of its own — an
/// end is one arm in the line's stroke, not a chosen glyph, because an end has to say where the
/// stroke stops and what may join it there, which an arm does and a literal cannot: a literal
/// composes as a cell with four `Closed` arms, so nothing connects into it and two lines meeting at
/// a shared end could never make the corner they make.
///
/// A **complete** shape in the sense of _Complete and fragment_ in
/// [`docs/model.md`](../../../docs/model.md). No minimum length is declared: a length of 0 draws
/// nothing, a length of 1 draws its one position as an end, and neither is rejected.
#[derive(Debug)]
pub struct Line {
    /// The position of the line's first cell.
    pub at: Pos,
    /// How many cells the line occupies. Any value is accepted, including 0.
    pub len: u16,
    /// Whether the line runs along a row or a column.
    pub orientation: Orientation,
    /// The stroke every cell this line writes is drawn in.
    pub stroke: Stroke,
}

impl Line {
    /// The position `i` cells along this line's orientation from `at`, or `None` if that would
    /// overflow `i16`.
    fn pos_at(&self, i: u16) -> Option<Pos> {
        match self.orientation {
            Orientation::Horizontal => self
                .at
                .x
                .checked_add_unsigned(i)
                .map(|x| Pos { x, y: self.at.y }),
            Orientation::Vertical => self
                .at
                .y
                .checked_add_unsigned(i)
                .map(|y| Pos { x: self.at.x, y }),
        }
    }
}

impl Shape for Line {
    fn draw(&self, surface: &mut dyn Surface) {
        if self.len == 0 {
            return;
        }
        let (forward, backward) = match self.orientation {
            Orientation::Horizontal => (Side::Right, Side::Left),
            Orientation::Vertical => (Side::Bottom, Side::Top),
        };

        if let Some(first) = self.pos_at(0) {
            End {
                at: first,
                side: forward,
                stroke: self.stroke.clone(),
            }
            .draw(surface);
        }
        if self.len >= 3
            && let Some(from) = self.pos_at(1)
        {
            Segment {
                from,
                len: self.len - 2,
                orientation: self.orientation,
                stroke: self.stroke.clone(),
            }
            .draw(surface);
        }
        if self.len >= 2
            && let Some(last) = self.pos_at(self.len - 1)
        {
            End {
                at: last,
                side: backward,
                stroke: self.stroke.clone(),
            }
            .draw(surface);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Line;
    use crate::shape::counting::CountingSurface;
    use crate::{
        Arm, Buffer, CellReader, FigureLayer, GlyphCatalog, Orientation, Pos, Shape, Size,
        StampMode, Stroke, Window, render,
    };

    fn light() -> Stroke {
        Stroke::from("light")
    }

    /// A horizontal line of length 5 renders `─────`, and the position
    /// one past its end holds no cell at all.
    #[test]
    fn a_horizontal_line_of_length_5_renders_and_stops_cleanly() {
        let origin = Pos { x: 0, y: 0 };
        let size = Size {
            width: 6,
            height: 1,
        };
        let mut buffer = Buffer::new(Window { origin, size });
        Line {
            at: origin,
            len: 5,
            orientation: Orientation::Horizontal,
            stroke: light(),
        }
        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));

        assert_eq!(
            render(&buffer, &GlyphCatalog::light(), buffer.window()),
            "───── \n"
        );
        assert!(buffer.cell(Pos { x: 5, y: 0 }).is_none());
    }

    /// A vertical line of length 4 renders four `│`s, and the position
    /// one past its end holds no cell.
    #[test]
    fn a_vertical_line_of_length_4_renders_and_stops_cleanly() {
        let origin = Pos { x: 0, y: 0 };
        let size = Size {
            width: 1,
            height: 5,
        };
        let mut buffer = Buffer::new(Window { origin, size });
        Line {
            at: origin,
            len: 4,
            orientation: Orientation::Vertical,
            stroke: light(),
        }
        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));

        assert_eq!(
            render(&buffer, &GlyphCatalog::light(), buffer.window()),
            "│\n│\n│\n│\n \n"
        );
        assert!(buffer.cell(Pos { x: 0, y: 4 }).is_none());
    }

    /// A horizontal line's first cell has its right arm `Set` and the
    /// other three `Unset` — it decides nothing about what lies beyond it. `Cell` is public for
    /// exactly this: a test that read the buffer back through `render` would depend on the catalog
    /// and on the lookup, so a wrong glyph would fail a test named after composition.
    #[test]
    fn a_horizontal_lines_first_cell_carries_only_its_forward_arm() {
        let origin = Pos { x: 0, y: 0 };
        let mut buffer = Buffer::new(Window {
            origin,
            size: Size {
                width: 5,
                height: 1,
            },
        });
        Line {
            at: origin,
            len: 5,
            orientation: Orientation::Horizontal,
            stroke: light(),
        }
        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));

        let Some(crate::Cell::Strokes(cell)) = buffer.cell(origin) else {
            panic!("expected a stroke cell at the line's first position");
        };
        assert_eq!(cell.right, Arm::Set(Stroke::from("light")));
        assert_eq!(cell.top, Arm::Unset);
        assert_eq!(cell.bottom, Arm::Unset);
        assert_eq!(cell.left, Arm::Unset);
    }

    /// A line of length 2 is two ends side by side, with no interior
    /// run between them.
    #[test]
    fn a_line_of_length_2_is_two_ends_with_no_run_between_them() {
        let origin = Pos { x: 0, y: 0 };
        let size = Size {
            width: 2,
            height: 1,
        };
        let mut buffer = Buffer::new(Window { origin, size });
        Line {
            at: origin,
            len: 2,
            orientation: Orientation::Horizontal,
            stroke: light(),
        }
        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));

        assert_eq!(
            render(&buffer, &GlyphCatalog::light(), buffer.window()),
            "──\n"
        );
    }

    /// A line of length 1 returns normally and writes exactly one
    /// position.
    #[test]
    fn a_line_of_length_1_writes_exactly_one_position() {
        let origin = Pos { x: 0, y: 0 };
        let mut buffer = Buffer::new(Window {
            origin,
            size: Size {
                width: 1,
                height: 1,
            },
        });
        Line {
            at: origin,
            len: 1,
            orientation: Orientation::Horizontal,
            stroke: light(),
        }
        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));

        assert!(buffer.cell(origin).is_some());
    }

    /// A line of length 0 draws nothing and returns normally.
    #[test]
    fn a_line_of_length_0_draws_nothing() {
        let origin = Pos { x: 0, y: 0 };
        let mut buffer = Buffer::new(Window {
            origin,
            size: Size {
                width: 1,
                height: 1,
            },
        });
        Line {
            at: origin,
            len: 0,
            orientation: Orientation::Horizontal,
            stroke: light(),
        }
        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));

        assert!(buffer.cell(origin).is_none());
    }

    /// No line above writes any position more than once, the
    /// length-1 line included.
    #[test]
    fn no_line_above_writes_any_position_more_than_once() {
        for len in [0, 1, 2, 3, 5] {
            let mut surface = CountingSurface::default();
            Line {
                at: Pos { x: 0, y: 0 },
                len,
                orientation: Orientation::Horizontal,
                stroke: light(),
            }
            .draw(&mut surface);

            assert!(
                surface.max_writes() <= 1,
                "length {len} wrote a position more than once"
            );
        }
    }

    /// A horizontal line and a vertical line whose ends land on
    /// `(0, 0)` render `┌` there — the corner the two make, not a segment, a T or a cross — in
    /// both drawing orders and under both stamp modes. This is the scenario a caller-supplied end
    /// glyph could not satisfy: a literal refuses every junction, so two lines meeting at a shared
    /// end would have nothing to connect.
    #[test]
    fn two_lines_sharing_an_end_compose_into_the_corner_they_make() {
        let origin = Pos { x: 0, y: 0 };
        let size = Size {
            width: 1,
            height: 1,
        };
        let horizontal = || Line {
            at: origin,
            len: 3,
            orientation: Orientation::Horizontal,
            stroke: light(),
        };
        let vertical = || Line {
            at: origin,
            len: 3,
            orientation: Orientation::Vertical,
            stroke: light(),
        };

        for mode in [StampMode::Above, StampMode::Below] {
            let mut horizontal_first = Buffer::new(Window { origin, size });
            horizontal().draw(&mut FigureLayer::new(
                &mut horizontal_first,
                StampMode::Above,
            ));
            vertical().draw(&mut FigureLayer::new(&mut horizontal_first, mode));

            let mut vertical_first = Buffer::new(Window { origin, size });
            vertical().draw(&mut FigureLayer::new(&mut vertical_first, StampMode::Above));
            horizontal().draw(&mut FigureLayer::new(&mut vertical_first, mode));

            for (label, buffer) in [
                ("horizontal first", &horizontal_first),
                ("vertical first", &vertical_first),
            ] {
                assert_eq!(
                    render(buffer, &GlyphCatalog::light(), buffer.window()),
                    "┌\n",
                    "{label}, second stamp under {mode:?}"
                );
            }
        }
    }
}