Skip to main content

monospace_core/
footprint.rs

1//! Measuring a drawing without keeping it. See _The buffer_ in
2//! [`docs/model.md`](../../../docs/model.md).
3
4use crate::{Cell, Footprint, Pos, ShapeId, StampMode, StampSink};
5
6/// A [`StampSink`] that measures the stamps passing through it and hands each one on.
7///
8/// **The footprint counts every stamp, not every cell.** A position the buffer behind it discards —
9/// outside its window — still moves the footprint, and so does a stamp the composition finds
10/// already decided: what is measured is how far the drawing reached, which is a fact of the traffic
11/// rather than of the picture that survived it. Accumulating before delegating is what makes those
12/// two the same question, and it is the whole of the rule this adapter carries.
13pub struct FootprintSink<'a> {
14    sink: &'a mut dyn StampSink,
15    footprint: Option<Footprint>,
16}
17
18impl<'a> FootprintSink<'a> {
19    /// Wraps `sink`, so that every stamp written through this one is measured and then drawn.
20    #[must_use]
21    pub fn new(sink: &'a mut dyn StampSink) -> Self {
22        Self {
23            sink,
24            footprint: None,
25        }
26    }
27
28    /// The rectangle the stamps written through this sink reach, or `None` while none has been.
29    ///
30    /// The answer is a value rather than a borrow, so a caller holds what it is given and may go on
31    /// stamping and ask again.
32    #[must_use]
33    pub fn footprint(&self) -> Option<Footprint> {
34        self.footprint
35    }
36}
37
38impl StampSink for FootprintSink<'_> {
39    fn stamp(&mut self, at: Pos, cell: Cell, mode: StampMode, owner: Option<ShapeId>) {
40        self.footprint = Some(match self.footprint {
41            None => Footprint { min: at, max: at },
42            Some(footprint) => footprint.including(at),
43        });
44        self.sink.stamp(at, cell, mode, owner);
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use std::num::NonZeroU16;
51
52    use super::FootprintSink;
53    use crate::{
54        Arm, Buffer, Cell, CellReader, Footprint, OwnerReader, Pos, ShapeId, Size, StampMode,
55        StampSink, Stroke, StrokeCell, Window,
56    };
57
58    /// A sink that keeps nothing and records what it was handed, so a test can say that the adapter
59    /// stood between a stamp and the store rather than in place of it.
60    #[derive(Default)]
61    struct Recording {
62        stamps: Vec<(Pos, StampMode, Option<ShapeId>)>,
63    }
64
65    impl StampSink for Recording {
66        fn stamp(&mut self, at: Pos, _cell: Cell, mode: StampMode, owner: Option<ShapeId>) {
67            self.stamps.push((at, mode, owner));
68        }
69    }
70
71    fn light() -> Stroke {
72        Stroke::from("light")
73    }
74
75    fn id(ordinal: u16) -> ShapeId {
76        ShapeId::new(NonZeroU16::new(ordinal).expect("no test asks for zero"))
77    }
78
79    /// A buffer `width` by `height` at `origin`, which is how every test below states where the
80    /// positions it asks about are.
81    fn a_buffer(origin: Pos, width: u16, height: u16) -> Buffer {
82        Buffer::new(Window {
83            origin,
84            size: Size { width, height },
85        })
86    }
87
88    /// A cell deciding all four sides, which is what makes it immutable under `Below`.
89    fn decided() -> Cell {
90        Cell::from(StrokeCell {
91            base: light(),
92            top: Arm::Set(light()),
93            right: Arm::Set(light()),
94            bottom: Arm::Set(light()),
95            left: Arm::Set(light()),
96        })
97    }
98
99    /// A cell deciding its left and right and abstaining on the other two — a horizontal run.
100    fn horizontal_run() -> Cell {
101        Cell::from(StrokeCell {
102            base: light(),
103            top: Arm::Unset,
104            right: Arm::Set(light()),
105            bottom: Arm::Unset,
106            left: Arm::Set(light()),
107        })
108    }
109
110    /// A cell deciding no side at all, which writes no owner and renders as a space.
111    fn deciding_nothing() -> Cell {
112        Cell::from(StrokeCell {
113            base: light(),
114            top: Arm::Unset,
115            right: Arm::Unset,
116            bottom: Arm::Unset,
117            left: Arm::Unset,
118        })
119    }
120
121    /// A sink nothing has been stamped into reaches nothing, and the first stamp is a one-cell
122    /// footprint wherever it lands.
123    ///
124    /// **`None` and a one-cell rectangle are different states** and the test says so on both sides:
125    /// a `Some` at the origin would be indistinguishable from a drawing that stamped `(0, 0)`.
126    #[test]
127    fn an_empty_drawing_has_no_footprint_and_the_first_stamp_is_one_cell() {
128        let mut recording = Recording::default();
129        let mut sink = FootprintSink::new(&mut recording);
130        assert_eq!(
131            sink.footprint(),
132            None,
133            "a drawing that stamped nothing reached somewhere"
134        );
135
136        let at = Pos { x: 2, y: 1 };
137        sink.stamp(at, decided(), StampMode::Above, None);
138
139        assert_eq!(sink.footprint(), Some(Footprint { min: at, max: at }));
140    }
141
142    /// Two stamps leave the union of both, and the footprint grows in every direction rather than
143    /// only rightward and downward: the second position is above and left of the first.
144    #[test]
145    fn the_footprint_is_the_union_of_every_stamp() {
146        let mut recording = Recording::default();
147        let mut sink = FootprintSink::new(&mut recording);
148
149        sink.stamp(Pos { x: 3, y: 4 }, decided(), StampMode::Above, None);
150        sink.stamp(Pos { x: -1, y: 2 }, decided(), StampMode::Above, None);
151
152        assert_eq!(
153            sink.footprint(),
154            Some(Footprint {
155                min: Pos { x: -1, y: 2 },
156                max: Pos { x: 3, y: 4 },
157            })
158        );
159    }
160
161    /// A stamp the buffer behind does not hold writes no cell and **still counts**: the window
162    /// decides what is stored, not how far the traffic reached.
163    ///
164    /// The position is behind and left of the window on both axes, so a record that only grew
165    /// rightward and downward — the shape a running maximum alone would take — fails here.
166    #[test]
167    fn a_stamp_outside_the_window_writes_no_cell_and_still_counts() {
168        let mut buffer = a_buffer(Pos { x: 0, y: 0 }, 1, 1);
169        let far = Pos { x: -5, y: -7 };
170        let footprint = {
171            let mut sink = FootprintSink::new(&mut buffer);
172            sink.stamp(far, decided(), StampMode::Above, Some(id(1)));
173            sink.footprint()
174        };
175
176        assert!(
177            buffer.cell(far).is_none(),
178            "the window wrote a cell it does not hold"
179        );
180        assert_eq!(footprint, Some(Footprint { min: far, max: far }));
181    }
182
183    /// A stamp that decides no side — and so owns nothing and renders as a space — **counts**, and
184    /// the footprint is the union of every stamp rather than of the cells that survived.
185    ///
186    /// This is the case that separates "how far the drawing reaches" from "what the picture shows",
187    /// and the two assertions are the two halves: no owner at the position, and the position is
188    /// still inside the footprint.
189    #[test]
190    fn a_stamp_that_decides_no_side_still_counts() {
191        let mut buffer = a_buffer(Pos { x: 0, y: 0 }, 3, 1);
192        let at = Pos { x: 0, y: 0 };
193        let footprint = {
194            let mut sink = FootprintSink::new(&mut buffer);
195            sink.stamp(at, deciding_nothing(), StampMode::Above, Some(id(1)));
196            sink.stamp(
197                Pos { x: 2, y: 0 },
198                horizontal_run(),
199                StampMode::Above,
200                Some(id(2)),
201            );
202            sink.footprint()
203        };
204
205        assert_eq!(
206            buffer.owner(Pos { x: 0, y: 0 }),
207            None,
208            "a cell deciding no side owns nothing"
209        );
210        assert_eq!(
211            footprint,
212            Some(Footprint {
213                min: Pos { x: 0, y: 0 },
214                max: Pos { x: 2, y: 0 },
215            })
216        );
217    }
218
219    /// A stamp that decides nothing, under a cell already decided, changes no cell and **still
220    /// counts**.
221    #[test]
222    fn a_stamp_below_a_decided_cell_changes_nothing_and_still_counts() {
223        let mut buffer = a_buffer(Pos { x: 0, y: 0 }, 1, 1);
224        let at = Pos { x: 0, y: 0 };
225        buffer.stamp(at, decided(), StampMode::Above, Some(id(1)));
226        let before = buffer.cell(at).cloned();
227
228        let footprint = {
229            let mut sink = FootprintSink::new(&mut buffer);
230            sink.stamp(at, deciding_nothing(), StampMode::Below, Some(id(2)));
231            sink.footprint()
232        };
233
234        assert_eq!(buffer.cell(at).cloned(), before, "the cell changed");
235        assert_eq!(footprint, Some(Footprint { min: at, max: at }));
236    }
237
238    /// Every stamp reaches the sink the adapter wraps, with the mode and the identity it was
239    /// written under: measuring is something done beside the write, not instead of it.
240    #[test]
241    fn every_stamp_reaches_the_sink_it_wraps() {
242        let mut recording = Recording::default();
243        let mut sink = FootprintSink::new(&mut recording);
244
245        sink.stamp(Pos { x: 1, y: 2 }, decided(), StampMode::Above, Some(id(3)));
246        sink.stamp(Pos { x: 4, y: 5 }, decided(), StampMode::Below, None);
247
248        assert_eq!(
249            recording.stamps,
250            vec![
251                (Pos { x: 1, y: 2 }, StampMode::Above, Some(id(3))),
252                (Pos { x: 4, y: 5 }, StampMode::Below, None),
253            ]
254        );
255    }
256}