Skip to main content

monospace_core/
cell.rs

1//! A cell: either a base stroke and four arms, one chosen glyph, or a symbol and the style it is
2//! drawn in. See _The cell_ in [`docs/model.md`](../../../docs/model.md).
3
4use crate::{Glyph, GlyphCatalog, GlyphKey, Stroke, SymbolCell};
5
6/// A place on a cell: one of its four sides.
7///
8/// Not the same thing as [`Direction`](crate::Direction), which is a way to move across the
9/// plane rather than a place on a cell — see _Vocabulary_ in
10/// [`docs/model.md`](../../../docs/model.md). Only the fragments in `crate::shape::fragment` name
11/// a `Side`; the figures above them reason in `Direction`s instead.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub(crate) enum Side {
14    /// The top of a cell.
15    Top,
16    /// The right of a cell.
17    Right,
18    /// The bottom of a cell.
19    Bottom,
20    /// The left of a cell.
21    Left,
22}
23
24/// What a cell has on one of its four sides.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Arm {
27    /// A stroke runs to this side, drawn in the stroke it carries.
28    Set(Stroke),
29    /// No stroke runs to this side, and that is decided.
30    Closed,
31    /// Not this stamp's to decide: whichever stamp writes here next chooses this side.
32    Unset,
33}
34
35/// A base stroke, always present, and four arms — one per side.
36///
37/// Each arm carries its own stroke. A cell once held a single stroke and drew every `Set` arm in
38/// it, which was a restriction on what a cell could express rather than a different rule, and the
39/// restriction was lifted when the reason for it ran out: a figure that crosses another needs a
40/// different stroke on each side, and a key built from one stroke can never name the two of them,
41/// so a light box crossing a double one drew the double's character and lost the light stroke.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct StrokeCell {
44    /// The stroke every `Set` arm draws in.
45    pub base: Stroke,
46    /// What the cell has on its top side.
47    pub top: Arm,
48    /// What the cell has on its right side.
49    pub right: Arm,
50    /// What the cell has on its bottom side.
51    pub bottom: Arm,
52    /// What the cell has on its left side.
53    pub left: Arm,
54}
55
56impl StrokeCell {
57    /// Whether every arm is decided: none of the four is `Unset`.
58    ///
59    /// A defined cell always has a base stroke, so nothing else enters the question.
60    #[must_use]
61    pub fn is_decided(&self) -> bool {
62        !matches!(self.top, Arm::Unset)
63            && !matches!(self.right, Arm::Unset)
64            && !matches!(self.bottom, Arm::Unset)
65            && !matches!(self.left, Arm::Unset)
66    }
67
68    /// The exact key this cell resolves to in a catalog: each arm's own stroke on every `Set`
69    /// side, and nothing where the arm is `Closed` or `Unset` — the two read the same at render
70    /// time.
71    #[must_use]
72    pub fn key(&self) -> GlyphKey {
73        let side = |arm: &Arm| match arm {
74            Arm::Set(stroke) => Some(stroke.clone()),
75            Arm::Closed | Arm::Unset => None,
76        };
77
78        GlyphKey {
79            top: side(&self.top),
80            right: side(&self.right),
81            bottom: side(&self.bottom),
82            left: side(&self.left),
83        }
84    }
85
86    /// The degraded key this cell falls back to when [`key`](Self::key) matches nothing: the
87    /// cell's own base stroke on every `Set` side, regardless of what that arm itself carries, and
88    /// nothing where the arm is `Closed` or `Unset`. Degrading the whole cell rather than the
89    /// smallest set of arms is what keeps "why that character?" one sentence — the figure that
90    /// owns the cell imposes its stroke on all of it — and what keeps the answer from depending on
91    /// which tables happen to be loaded.
92    #[must_use]
93    pub fn degraded_key(&self) -> GlyphKey {
94        let side = |arm: &Arm| match arm {
95            Arm::Set(_) => Some(self.base.clone()),
96            Arm::Closed | Arm::Unset => None,
97        };
98
99        GlyphKey {
100            top: side(&self.top),
101            right: side(&self.right),
102            bottom: side(&self.bottom),
103            left: side(&self.left),
104        }
105    }
106}
107
108/// A cell: either a base stroke with four arms, one chosen glyph, or a named symbol rendered
109/// through the catalog. The three kinds are mutually exclusive — no cell is two and none is
110/// neither — per _The cell_ in [`docs/model.md`](../../../docs/model.md).
111///
112/// Where a figure in front stamps over one behind, **the kind on top decides which kind the
113/// position ends up being**, exactly as it decides the base stroke of a [`StrokeCell`]: see
114/// _Stamping_ in [`docs/model.md`](../../../docs/model.md).
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum Cell {
117    /// A base stroke and four arms.
118    Strokes(StrokeCell),
119    /// One chosen glyph. Nothing connects into it, and it renders as itself without consulting
120    /// any catalog.
121    Literal(Glyph),
122    /// A symbol and the style it is drawn in. Nothing connects into it, and the catalog the
123    /// renderer was handed answers the pair — or does not, which is a space.
124    Symbol(SymbolCell),
125}
126
127impl From<StrokeCell> for Cell {
128    fn from(cell: StrokeCell) -> Self {
129        Cell::Strokes(cell)
130    }
131}
132
133impl Cell {
134    /// Whether the cell is already decided. For [`Cell::Strokes`] this is
135    /// [`StrokeCell::is_decided`]; a [`Cell::Literal`] and a [`Cell::Symbol`] are decided on all
136    /// four sides by definition, since neither has an arm to leave undecided.
137    #[must_use]
138    pub fn is_decided(&self) -> bool {
139        match self {
140            Cell::Strokes(cell) => cell.is_decided(),
141            Cell::Literal(_) | Cell::Symbol(_) => true,
142        }
143    }
144
145    /// The text this cell renders to. A literal answers with its own text directly, without
146    /// consulting `glyphs`; a symbol answers with the glyph filed under its pair, and nothing if
147    /// the catalog holds no such pair; a stroke cell is looked up by [`StrokeCell::key`] first
148    /// and [`StrokeCell::degraded_key`] second, answering `None` if `glyphs` has no rule for
149    /// either. See _Rendering_ in [`docs/model.md`](../../../docs/model.md).
150    #[must_use]
151    pub fn glyph_str<'a>(&'a self, glyphs: &'a GlyphCatalog) -> Option<&'a str> {
152        match self {
153            Cell::Literal(glyph) => Some(glyph.as_str()),
154            Cell::Symbol(symbol) => glyphs.symbol(symbol).map(Glyph::as_str),
155            Cell::Strokes(cell) => glyphs
156                .glyph(&cell.key())
157                .or_else(|| glyphs.glyph(&cell.degraded_key()))
158                .map(Glyph::as_str),
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::{Cell, StrokeCell};
166    use crate::{Arm, Glyph, GlyphCatalog, Stroke, Symbol, SymbolCell, SymbolStyleName};
167
168    fn cell(top: Arm, right: Arm, bottom: Arm, left: Arm) -> StrokeCell {
169        StrokeCell {
170            base: Stroke::from("light"),
171            top,
172            right,
173            bottom,
174            left,
175        }
176    }
177
178    /// The example named "The boundaries": one abstention is enough to make a cell not decided.
179    /// Unchanged from before the literal existed: a stroke cell is decided by its arms alone.
180    #[test]
181    fn a_stroke_cell_is_decided_only_when_no_arm_is_unset() {
182        let light = || Arm::Set(Stroke::from("light"));
183        assert!(cell(light(), Arm::Closed, light(), Arm::Closed).is_decided());
184        assert!(!cell(Arm::Unset, light(), Arm::Closed, light()).is_decided());
185        assert!(!cell(light(), light(), light(), Arm::Unset).is_decided());
186    }
187
188    /// A literal has no arms to leave `Unset`, so it is decided by definition.
189    #[test]
190    fn a_literal_is_always_decided() {
191        let literal = Cell::Literal(Glyph::new("A").expect("\"A\" is one glyph"));
192
193        assert!(literal.is_decided());
194    }
195
196    /// A pair with `symbol` drawn in `style`, so a test can name one without the whole struct
197    /// spelled at every call.
198    fn a_symbol(symbol: Symbol, style: &str) -> SymbolCell {
199        SymbolCell {
200            symbol,
201            style: SymbolStyleName::from(style),
202        }
203    }
204
205    /// Rule 1 — a symbol cell renders as the glyph filed under its exact pair, and a pair the
206    /// catalog does not hold answers nothing: no second lookup and no degradation.
207    #[test]
208    fn a_symbol_cell_answers_its_exact_pair_and_a_missing_pair_is_nothing() {
209        let solid_up = a_symbol(Symbol::DirectionUp, "solid");
210        let cell = Cell::Symbol(solid_up.clone());
211        let catalog = GlyphCatalog::solid();
212
213        assert_eq!(cell.glyph_str(&catalog), Some("▲"));
214        assert_eq!(
215            Cell::Symbol(a_symbol(Symbol::DirectionUp, "outline")).glyph_str(&catalog),
216            None,
217            "a style the catalog does not hold is not answered from another one"
218        );
219        assert_eq!(
220            Cell::Symbol(a_symbol(Symbol::Diamond, "nonsense")).glyph_str(&catalog),
221            None,
222            "a misspelled style and a missing shape answer alike"
223        );
224        assert_eq!(
225            Cell::Symbol(solid_up).glyph_str(&GlyphCatalog::light()),
226            None,
227            "a catalog with no symbol rule answers no pair at all"
228        );
229    }
230
231    /// Rule 2 — a symbol cell is decided on every side: it has no arm to leave `Unset`, and the
232    /// renderer reads no stroke from it.
233    #[test]
234    fn a_symbol_cell_writes_no_arm_and_is_decided_on_every_side() {
235        let cell = Cell::Symbol(a_symbol(Symbol::Circle, "solid"));
236
237        assert!(cell.is_decided());
238    }
239
240    /// Rule 4 — a symbol cell and the literal drawing the same character render alike and are
241    /// still two cells, because equality is by the pair rather than by what it draws.
242    #[test]
243    fn a_symbol_cell_and_the_literal_that_draws_it_render_alike_and_differ() {
244        let named = Cell::Symbol(a_symbol(Symbol::DirectionUp, "solid"));
245        let spelling = Cell::Literal(Glyph::new("▲").expect("\"▲\" is one glyph"));
246        let catalog = GlyphCatalog::solid();
247
248        assert_eq!(named.glyph_str(&catalog), spelling.glyph_str(&catalog));
249        assert_ne!(named, spelling);
250    }
251
252    fn mixed_cell() -> StrokeCell {
253        StrokeCell {
254            base: Stroke::from("light"),
255            top: Arm::Set(Stroke::from("light")),
256            right: Arm::Set(Stroke::from("heavy")),
257            bottom: Arm::Set(Stroke::from("light")),
258            left: Arm::Set(Stroke::from("heavy")),
259        }
260    }
261
262    /// `key()` reads each arm's own stroke, not the cell's `base`.
263    #[test]
264    fn key_reads_each_arms_own_stroke() {
265        let key = mixed_cell().key();
266
267        assert_eq!(key.top, Some(Stroke::from("light")));
268        assert_eq!(key.bottom, Some(Stroke::from("light")));
269        assert_eq!(key.right, Some(Stroke::from("heavy")));
270        assert_eq!(key.left, Some(Stroke::from("heavy")));
271    }
272
273    /// `degraded_key()` names the cell's `base` stroke on every `Set` side regardless of
274    /// what that arm itself carries.
275    #[test]
276    fn degraded_key_collapses_every_set_arm_to_the_base_stroke() {
277        let key = mixed_cell().degraded_key();
278
279        assert_eq!(key.top, Some(Stroke::from("light")));
280        assert_eq!(key.bottom, Some(Stroke::from("light")));
281        assert_eq!(key.right, Some(Stroke::from("light")));
282        assert_eq!(key.left, Some(Stroke::from("light")));
283    }
284
285    /// `glyph_str` tries the exact key, then the degraded
286    /// key, then answers `None`.
287    #[test]
288    fn glyph_str_tries_the_exact_key_then_the_degraded_key_then_none() {
289        let mixed_glyph = Glyph::new("┿").expect("\"┿\" is one glyph");
290        let degraded_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
291        let cell: Cell = mixed_cell().into();
292
293        let exact = GlyphCatalog::from_rules([(mixed_cell().key(), mixed_glyph.clone())]);
294        assert_eq!(cell.glyph_str(&exact), Some(mixed_glyph.as_str()));
295
296        let degraded_only =
297            GlyphCatalog::from_rules([(mixed_cell().degraded_key(), degraded_glyph.clone())]);
298        assert_eq!(
299            cell.glyph_str(&degraded_only),
300            Some(degraded_glyph.as_str())
301        );
302
303        let neither = GlyphCatalog::from_rules([]);
304        assert_eq!(cell.glyph_str(&neither), None);
305    }
306
307    /// A cell whose arms all carry the base stroke
308    /// resolves the same key from both methods.
309    #[test]
310    fn a_cell_whose_arms_all_carry_the_base_stroke_resolves_the_same_key_from_both_methods() {
311        let uniform = cell(
312            Arm::Set(Stroke::from("light")),
313            Arm::Set(Stroke::from("light")),
314            Arm::Set(Stroke::from("light")),
315            Arm::Set(Stroke::from("light")),
316        );
317
318        assert_eq!(uniform.key(), uniform.degraded_key());
319    }
320
321    /// An uncovered mixture degrades to the base stroke
322    /// regardless of a partial mixing rule set covering some, but not this, combination.
323    #[test]
324    fn an_uncovered_mixture_degrades_to_the_base_stroke_regardless_of_a_partial_mixing_rule_set() {
325        let base_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
326        let cell: Cell = mixed_cell().into();
327
328        let without_partial_rules =
329            GlyphCatalog::from_rules([(mixed_cell().degraded_key(), base_glyph.clone())]);
330        let without_result = cell.glyph_str(&without_partial_rules);
331
332        let unrelated_key = crate::GlyphKey {
333            top: Some(Stroke::from("double")),
334            right: Some(Stroke::from("double")),
335            bottom: Some(Stroke::from("double")),
336            left: Some(Stroke::from("double")),
337        };
338        let with_partial_rules = GlyphCatalog::from_rules([
339            (mixed_cell().degraded_key(), base_glyph.clone()),
340            (unrelated_key, Glyph::new("╬").expect("\"╬\" is one glyph")),
341        ]);
342        let with_result = cell.glyph_str(&with_partial_rules);
343
344        assert_eq!(without_result, Some(base_glyph.as_str()));
345        assert_eq!(with_result, without_result);
346    }
347
348    /// `light` and `light-round` degrade with no mixing table
349    /// pairing them.
350    #[test]
351    fn light_and_light_round_degrade_with_no_mixing_table_pairing_them() {
352        let mixed = cell(
353            Arm::Set(Stroke::from("light")),
354            Arm::Set(Stroke::from("light-round")),
355            Arm::Set(Stroke::from("light")),
356            Arm::Set(Stroke::from("light-round")),
357        );
358        let cell_value: Cell = mixed.clone().into();
359        let base_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
360        let catalog = GlyphCatalog::from_rules([(mixed.degraded_key(), base_glyph.clone())]);
361
362        assert_eq!(cell_value.glyph_str(&catalog), Some(base_glyph.as_str()));
363    }
364
365    /// `heavy` and `double` degrade with no mixing table
366    /// pairing them.
367    #[test]
368    fn heavy_and_double_degrade_with_no_mixing_table_pairing_them() {
369        let mixed = cell(
370            Arm::Set(Stroke::from("heavy")),
371            Arm::Set(Stroke::from("double")),
372            Arm::Set(Stroke::from("heavy")),
373            Arm::Set(Stroke::from("double")),
374        );
375        let cell_value: Cell = mixed.clone().into();
376        let base_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
377        let catalog = GlyphCatalog::from_rules([(mixed.degraded_key(), base_glyph.clone())]);
378
379        assert_eq!(cell_value.glyph_str(&catalog), Some(base_glyph.as_str()));
380    }
381}