//! The house glyph set, as data. //! //! The set is the durable artifact and this module is only its schema. Shapes //! are named recipes with ratio parameters; nothing here knows about a font. use serde::Deserialize; use crate::Error; /// A dimension, in the set's only unit: a share of the base's symbol band plus /// a term that follows the base's stroke weight. /// /// ```text /// value = band_extent * band + base_stroke * weight /// ``` /// /// Plex Mono redraws its own symbols heavier in Bold rather than merely /// thickening them in place: `•` goes 244 to 308 units wide, `×` 424 to 460, /// `↑` 414 to 478. Only the block elements hold still, and those are cell-fill /// primitives rather than symbols. A mark sized off the band alone would /// therefore stay Regular's size inside a Bold face and read light among its /// neighbours. /// /// Written as a bare number when the weight term is zero. #[derive(Debug, Clone, Copy)] pub struct Dim { pub band: f64, pub weight: f64, } impl Dim { pub fn resolve(self, band_extent: f64, base_stroke: f64) -> f64 { band_extent * self.band + base_stroke * self.weight } } impl<'de> Deserialize<'de> for Dim { fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(untagged)] enum Raw { Flat(f64), Pair { band: f64, #[serde(default)] weight: f64, }, } Ok(match Raw::deserialize(deserializer)? { Raw::Flat(band) => Dim { band, weight: 0.0 }, Raw::Pair { band, weight } => Dim { band, weight }, }) } } #[derive(Debug, Deserialize)] pub struct Manifest { pub set: SetMeta, #[serde(default, rename = "glyph")] pub glyphs: Vec, /// Whole Unicode blocks taken as generated cell primitives. /// /// A block is asked for by name rather than written out, because its 128 /// recipes are already written down in Unicode's own character names and /// transcribing them here would be 128 chances to transpose two arms. See /// [`crate::cells`]. #[serde(default, rename = "generated")] pub generated: Vec, } #[derive(Debug, Deserialize)] pub struct Generated { pub block: crate::cells::Block, } #[derive(Debug, Deserialize)] pub struct SetMeta { pub version: u32, pub name: String, } #[derive(Debug, Deserialize)] pub struct GlyphSpec { /// The codepoint the mark is mapped at. pub codepoint: u32, /// The glyph's production name. `uniXXXX` by convention. pub name: String, /// What the mark is for, in one line. Read by `quasi-type list`. pub role: String, /// Where the requirement came from, when there is a decision behind it. #[serde(default)] pub source: Option, /// Why the set carries this entry, which decides what happens when the /// base turns out to draw it too. #[serde(default)] pub purpose: Purpose, #[serde(flatten)] pub shape: Shape, } /// Why an entry is in the set. /// /// The set holds two kinds of thing and they want opposite answers to one /// question: what should happen when the base already draws this codepoint? /// /// A **house** mark is here because the drawing has to be the same in a /// terminal, a webview and an egui panel. A base that turns out to draw it is a /// fact somebody has to look at, because our drawing is about to be dropped in /// favour of one nobody compared it to. So it is an error, and the run says /// which glyph and which base. /// /// A **coverage** entry is here because a consumer needs the codepoint to /// exist, and any competent drawing of it will do. Whether the base has one /// varies by base and not by anything we decide: Plex Mono draws all 128 /// box-drawing glyphs and all four arrows, Atkinson Hyperlegible Mono draws /// none of them. Erroring would mean the same set worked against one base and /// not the other, and the fix would be a per-slot list of codepoints mirroring /// each base's coverage — bookkeeping that rots the first time an upstream adds /// a glyph. So the base keeps its own, which is also the better drawing, and /// the build reports how many it kept. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Purpose { /// The house drawing, and it must be ours. #[default] House, /// The codepoint has to exist; the base's own drawing is welcome to it. Coverage, } /// A drawing recipe. Every dimension is a ratio of something measured off the /// base face, so a shape is refitted to the next base rather than redrawn. #[derive(Debug, Deserialize)] #[serde(tag = "shape", rename_all = "kebab-case")] pub enum Shape { /// A solid triangle. Triangle { direction: Direction, /// Extent across the base edge. span: Dim, /// Apex distance, as a ratio of `span`. A pure ratio, so 0.866 stays /// equilateral at every weight. depth: f64, #[serde(default)] anchor: Anchor, }, /// Two crossed strokes. Cross { width: Dim, height: Dim, /// Stroke as a multiple of the base's own bar. stroke: f64, }, /// A `U` open at the top: U+2423's usual drawing. OpenBox { width: Dim, height: Dim, /// Distance above the baseline, as a ratio of the band's height. bottom: f64, /// Stroke as a multiple of the base's own, since the mark is a stroke. stroke: f64, }, /// A shaft with a solid head, and optionally a second head at the other /// end: the arrow set Alloy's own configs emit. /// /// A stroked mark rather than a solid one, so it holds its extent and /// thickens, the way the base does `+`. The base's own `↑` grows about 15% /// across its weights as well; the set does not follow it there, because an /// arrow whose shaft thickens and whose length also grew would be two /// responses at once and neither one legible as the rule. Arrow { direction: Direction, /// A head at the tail as well, for `↕`. #[serde(default)] both_ends: bool, /// Overall length along the arrow's own axis. length: Dim, /// The head's extent across the shaft. head_span: Dim, /// The head's depth along the shaft, as a ratio of `head_span`. head_depth: f64, /// Shaft thickness, as a multiple of the base's own bar. stroke: f64, }, /// A generated cell primitive: box drawing or a block element. /// /// Not written in the manifest and not deserializable from it. It arrives /// through a `[[generated]]` block, which is what keeps 160 recipes out of /// a file whose job is to hold the ones somebody chose. #[serde(skip)] Cell(crate::cells::Cell), /// A left-pointing arrow with a riser at its right end: U+23CE. ReturnArrow { /// The riser's flat cap, as a ratio of the band's height. top: f64, /// The shaft's centre, as a ratio of the band's height above its floor. shaft: f64, /// Arrowhead extent across the shaft, off the band's height. head_span: Dim, /// Arrowhead depth along the shaft, off the band's width. head_depth: Dim, /// Stroke as a multiple of the base's own. stroke: f64, }, } impl Shape { pub fn kind(&self) -> &'static str { match self { Shape::Arrow { .. } => "arrow", Shape::Cell(cell) => cell.kind(), Shape::Triangle { .. } => "triangle", Shape::Cross { .. } => "cross", Shape::OpenBox { .. } => "open-box", Shape::ReturnArrow { .. } => "return-arrow", } } /// Whether this is cell furniture rather than a band-relative mark. /// /// The two tiers are sized against different things and obey different /// rules — a vertical bar runs to the descender and triples in width with /// the base's stem, and both would be defects in an authored mark. So the /// assertions that hold for the manifest's own marks are scoped with this /// rather than loosened to accommodate furniture. See [`crate::cells`], /// which carries the cell tier's own, stricter set. pub fn is_cell_furniture(&self) -> bool { matches!(self, Shape::Cell(_)) } /// How a mark answers a heavier base. /// /// Everything in the set answers; the two kinds differ in what moves. A /// solid mark has no stroke to thicken, so it grows, exactly as the base /// grows `•` and `×`. A stroked mark holds its extent and thickens, which /// is what the base does to `+`: same bbox in both weights, heavier bars. pub fn weight_response(&self) -> WeightResponse { match self { Shape::Cell(cell) => cell.weight_response(), Shape::Triangle { .. } => WeightResponse::Grows, Shape::Arrow { .. } | Shape::Cross { .. } | Shape::OpenBox { .. } | Shape::ReturnArrow { .. } => WeightResponse::Thickens, } } } /// What a heavier base moves in a mark. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WeightResponse { /// The mark's extent grows; it has no stroke to thicken. Grows, /// The mark holds its extent and its stroke thickens. Thickens, /// The mark does not answer weight at all. /// /// Only the cell fills. `█` is the whole cell and `▄` is exactly half of /// it at every weight, which is what the base does with its own: they are /// cell-fill primitives rather than symbols, and this is the observation /// the set's weight term was first, wrongly, generalised from. Holds, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Direction { Up, Down, Left, Right, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Anchor { /// Centred on the symbol band, which is where the base puts `+`. #[default] BandCenter, /// Centred on the x-height, for marks that sit among lowercase. XHeight, } impl Manifest { pub fn parse(source: &str) -> Result { let mut manifest: Manifest = toml::from_str(source).map_err(|e| Error::Manifest(e.to_string()))?; for generated in &manifest.generated { manifest.glyphs.extend(crate::cells::specs(generated.block)); } manifest.glyphs.sort_by_key(|glyph| glyph.codepoint); let mut seen: Vec = Vec::new(); for glyph in &manifest.glyphs { if char::from_u32(glyph.codepoint).is_none() { return Err(Error::Manifest(format!( "{} is not a Unicode scalar value", format_codepoint(glyph.codepoint) ))); } if seen.contains(&glyph.codepoint) { return Err(Error::Manifest(format!( "{} appears twice in the set", format_codepoint(glyph.codepoint) ))); } seen.push(glyph.codepoint); } Ok(manifest) } } pub fn format_codepoint(codepoint: u32) -> String { format!("U+{codepoint:04X}") } #[cfg(test)] mod tests { use super::*; #[test] fn the_shipped_set_parses() { let manifest = Manifest::parse(crate::HOUSE_SET).expect("the shipped set parses"); let authored = manifest .glyphs .iter() .filter(|glyph| !glyph.shape.is_cell_furniture()) .count(); assert_eq!( authored, 16, "the seven marks, five arrows and four scroll markers" ); assert_eq!( manifest.glyphs.len(), 176, "plus both generated blocks, 160 of them" ); } #[test] fn a_repeated_codepoint_is_rejected() { let source = r#" [set] version = 1 name = "t" [[glyph]] codepoint = 0x25B2 name = "a" role = "r" shape = "triangle" direction = "up" span = 1.0 depth = 1.0 [[glyph]] codepoint = 0x25B2 name = "b" role = "r" shape = "triangle" direction = "down" span = 1.0 depth = 1.0 "#; let err = Manifest::parse(source).unwrap_err(); assert!(err.to_string().contains("twice"), "{err}"); } #[test] fn solid_marks_grow_and_stroked_marks_thicken() { let manifest = Manifest::parse(crate::HOUSE_SET).unwrap(); for glyph in manifest .glyphs .iter() .filter(|glyph| !glyph.shape.is_cell_furniture()) { let expected = match glyph.shape.kind() { "triangle" => WeightResponse::Grows, _ => WeightResponse::Thickens, }; assert_eq!( glyph.shape.weight_response(), expected, "{} ({})", glyph.name, glyph.shape.kind() ); } } #[test] fn a_dimension_reads_as_a_bare_number_or_as_a_pair() { let flat: Dim = toml::from_str::("v = 0.5").unwrap()["v"] .clone() .try_into() .unwrap(); assert!((flat.band - 0.5).abs() < 1e-9 && flat.weight == 0.0); let pair: Dim = toml::from_str::("v = { band = 0.5, weight = 0.6 }").unwrap() ["v"] .clone() .try_into() .unwrap(); assert!((pair.band - 0.5).abs() < 1e-9 && (pair.weight - 0.6).abs() < 1e-9); // 476 units of band and an 84-unit stroke: Plex Mono Regular. assert!((pair.resolve(476.0, 84.0) - (476.0 * 0.5 + 84.0 * 0.6)).abs() < 1e-9); } }