use crate::{Glyph, GlyphCatalog, GlyphKey, Stroke, SymbolCell};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Side {
Top,
Right,
Bottom,
Left,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Arm {
Set(Stroke),
Closed,
Unset,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StrokeCell {
pub base: Stroke,
pub top: Arm,
pub right: Arm,
pub bottom: Arm,
pub left: Arm,
}
impl StrokeCell {
#[must_use]
pub fn is_decided(&self) -> bool {
!matches!(self.top, Arm::Unset)
&& !matches!(self.right, Arm::Unset)
&& !matches!(self.bottom, Arm::Unset)
&& !matches!(self.left, Arm::Unset)
}
#[must_use]
pub fn key(&self) -> GlyphKey {
let side = |arm: &Arm| match arm {
Arm::Set(stroke) => Some(stroke.clone()),
Arm::Closed | Arm::Unset => None,
};
GlyphKey {
top: side(&self.top),
right: side(&self.right),
bottom: side(&self.bottom),
left: side(&self.left),
}
}
#[must_use]
pub fn degraded_key(&self) -> GlyphKey {
let side = |arm: &Arm| match arm {
Arm::Set(_) => Some(self.base.clone()),
Arm::Closed | Arm::Unset => None,
};
GlyphKey {
top: side(&self.top),
right: side(&self.right),
bottom: side(&self.bottom),
left: side(&self.left),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cell {
Strokes(StrokeCell),
Literal(Glyph),
Symbol(SymbolCell),
}
impl From<StrokeCell> for Cell {
fn from(cell: StrokeCell) -> Self {
Cell::Strokes(cell)
}
}
impl Cell {
#[must_use]
pub fn is_decided(&self) -> bool {
match self {
Cell::Strokes(cell) => cell.is_decided(),
Cell::Literal(_) | Cell::Symbol(_) => true,
}
}
#[must_use]
pub fn glyph_str<'a>(&'a self, glyphs: &'a GlyphCatalog) -> Option<&'a str> {
match self {
Cell::Literal(glyph) => Some(glyph.as_str()),
Cell::Symbol(symbol) => glyphs.symbol(symbol).map(Glyph::as_str),
Cell::Strokes(cell) => glyphs
.glyph(&cell.key())
.or_else(|| glyphs.glyph(&cell.degraded_key()))
.map(Glyph::as_str),
}
}
}
#[cfg(test)]
mod tests {
use super::{Cell, StrokeCell};
use crate::{Arm, Glyph, GlyphCatalog, Stroke, Symbol, SymbolCell, SymbolStyleName};
fn cell(top: Arm, right: Arm, bottom: Arm, left: Arm) -> StrokeCell {
StrokeCell {
base: Stroke::from("light"),
top,
right,
bottom,
left,
}
}
#[test]
fn a_stroke_cell_is_decided_only_when_no_arm_is_unset() {
let light = || Arm::Set(Stroke::from("light"));
assert!(cell(light(), Arm::Closed, light(), Arm::Closed).is_decided());
assert!(!cell(Arm::Unset, light(), Arm::Closed, light()).is_decided());
assert!(!cell(light(), light(), light(), Arm::Unset).is_decided());
}
#[test]
fn a_literal_is_always_decided() {
let literal = Cell::Literal(Glyph::new("A").expect("\"A\" is one glyph"));
assert!(literal.is_decided());
}
fn a_symbol(symbol: Symbol, style: &str) -> SymbolCell {
SymbolCell {
symbol,
style: SymbolStyleName::from(style),
}
}
#[test]
fn a_symbol_cell_answers_its_exact_pair_and_a_missing_pair_is_nothing() {
let solid_up = a_symbol(Symbol::DirectionUp, "solid");
let cell = Cell::Symbol(solid_up.clone());
let catalog = GlyphCatalog::solid();
assert_eq!(cell.glyph_str(&catalog), Some("▲"));
assert_eq!(
Cell::Symbol(a_symbol(Symbol::DirectionUp, "outline")).glyph_str(&catalog),
None,
"a style the catalog does not hold is not answered from another one"
);
assert_eq!(
Cell::Symbol(a_symbol(Symbol::Diamond, "nonsense")).glyph_str(&catalog),
None,
"a misspelled style and a missing shape answer alike"
);
assert_eq!(
Cell::Symbol(solid_up).glyph_str(&GlyphCatalog::light()),
None,
"a catalog with no symbol rule answers no pair at all"
);
}
#[test]
fn a_symbol_cell_writes_no_arm_and_is_decided_on_every_side() {
let cell = Cell::Symbol(a_symbol(Symbol::Circle, "solid"));
assert!(cell.is_decided());
}
#[test]
fn a_symbol_cell_and_the_literal_that_draws_it_render_alike_and_differ() {
let named = Cell::Symbol(a_symbol(Symbol::DirectionUp, "solid"));
let spelling = Cell::Literal(Glyph::new("▲").expect("\"▲\" is one glyph"));
let catalog = GlyphCatalog::solid();
assert_eq!(named.glyph_str(&catalog), spelling.glyph_str(&catalog));
assert_ne!(named, spelling);
}
fn mixed_cell() -> StrokeCell {
StrokeCell {
base: Stroke::from("light"),
top: Arm::Set(Stroke::from("light")),
right: Arm::Set(Stroke::from("heavy")),
bottom: Arm::Set(Stroke::from("light")),
left: Arm::Set(Stroke::from("heavy")),
}
}
#[test]
fn key_reads_each_arms_own_stroke() {
let key = mixed_cell().key();
assert_eq!(key.top, Some(Stroke::from("light")));
assert_eq!(key.bottom, Some(Stroke::from("light")));
assert_eq!(key.right, Some(Stroke::from("heavy")));
assert_eq!(key.left, Some(Stroke::from("heavy")));
}
#[test]
fn degraded_key_collapses_every_set_arm_to_the_base_stroke() {
let key = mixed_cell().degraded_key();
assert_eq!(key.top, Some(Stroke::from("light")));
assert_eq!(key.bottom, Some(Stroke::from("light")));
assert_eq!(key.right, Some(Stroke::from("light")));
assert_eq!(key.left, Some(Stroke::from("light")));
}
#[test]
fn glyph_str_tries_the_exact_key_then_the_degraded_key_then_none() {
let mixed_glyph = Glyph::new("┿").expect("\"┿\" is one glyph");
let degraded_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
let cell: Cell = mixed_cell().into();
let exact = GlyphCatalog::from_rules([(mixed_cell().key(), mixed_glyph.clone())]);
assert_eq!(cell.glyph_str(&exact), Some(mixed_glyph.as_str()));
let degraded_only =
GlyphCatalog::from_rules([(mixed_cell().degraded_key(), degraded_glyph.clone())]);
assert_eq!(
cell.glyph_str(°raded_only),
Some(degraded_glyph.as_str())
);
let neither = GlyphCatalog::from_rules([]);
assert_eq!(cell.glyph_str(&neither), None);
}
#[test]
fn a_cell_whose_arms_all_carry_the_base_stroke_resolves_the_same_key_from_both_methods() {
let uniform = cell(
Arm::Set(Stroke::from("light")),
Arm::Set(Stroke::from("light")),
Arm::Set(Stroke::from("light")),
Arm::Set(Stroke::from("light")),
);
assert_eq!(uniform.key(), uniform.degraded_key());
}
#[test]
fn an_uncovered_mixture_degrades_to_the_base_stroke_regardless_of_a_partial_mixing_rule_set() {
let base_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
let cell: Cell = mixed_cell().into();
let without_partial_rules =
GlyphCatalog::from_rules([(mixed_cell().degraded_key(), base_glyph.clone())]);
let without_result = cell.glyph_str(&without_partial_rules);
let unrelated_key = crate::GlyphKey {
top: Some(Stroke::from("double")),
right: Some(Stroke::from("double")),
bottom: Some(Stroke::from("double")),
left: Some(Stroke::from("double")),
};
let with_partial_rules = GlyphCatalog::from_rules([
(mixed_cell().degraded_key(), base_glyph.clone()),
(unrelated_key, Glyph::new("╬").expect("\"╬\" is one glyph")),
]);
let with_result = cell.glyph_str(&with_partial_rules);
assert_eq!(without_result, Some(base_glyph.as_str()));
assert_eq!(with_result, without_result);
}
#[test]
fn light_and_light_round_degrade_with_no_mixing_table_pairing_them() {
let mixed = cell(
Arm::Set(Stroke::from("light")),
Arm::Set(Stroke::from("light-round")),
Arm::Set(Stroke::from("light")),
Arm::Set(Stroke::from("light-round")),
);
let cell_value: Cell = mixed.clone().into();
let base_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
let catalog = GlyphCatalog::from_rules([(mixed.degraded_key(), base_glyph.clone())]);
assert_eq!(cell_value.glyph_str(&catalog), Some(base_glyph.as_str()));
}
#[test]
fn heavy_and_double_degrade_with_no_mixing_table_pairing_them() {
let mixed = cell(
Arm::Set(Stroke::from("heavy")),
Arm::Set(Stroke::from("double")),
Arm::Set(Stroke::from("heavy")),
Arm::Set(Stroke::from("double")),
);
let cell_value: Cell = mixed.clone().into();
let base_glyph = Glyph::new("┼").expect("\"┼\" is one glyph");
let catalog = GlyphCatalog::from_rules([(mixed.degraded_key(), base_glyph.clone())]);
assert_eq!(cell_value.glyph_str(&catalog), Some(base_glyph.as_str()));
}
}