1use crate::{Glyph, GlyphCatalog, GlyphKey, Stroke, SymbolCell};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub(crate) enum Side {
14 Top,
16 Right,
18 Bottom,
20 Left,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Arm {
27 Set(Stroke),
29 Closed,
31 Unset,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct StrokeCell {
44 pub base: Stroke,
46 pub top: Arm,
48 pub right: Arm,
50 pub bottom: Arm,
52 pub left: Arm,
54}
55
56impl StrokeCell {
57 #[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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum Cell {
117 Strokes(StrokeCell),
119 Literal(Glyph),
122 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 #[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 #[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 #[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 #[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 fn a_symbol(symbol: Symbol, style: &str) -> SymbolCell {
199 SymbolCell {
200 symbol,
201 style: SymbolStyleName::from(style),
202 }
203 }
204
205 #[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 #[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 #[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 #[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 #[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 #[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(°raded_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 #[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 #[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 #[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 #[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}