Skip to main content

rusty_figlet/
layout.rs

1//! Layout-mode resolution per AD-009 + HINT-002.
2//!
3//! Collapses the user-supplied `-k`/`-W`/`-S`/`-s`/`-o`/`-m N` and
4//! `-c`/`-l`/`-r`/`-x` flag occurrences into a single
5//! [`LayoutMode`] + [`Justify`] pair using last-wins semantics.
6
7use crate::figfont::FIGfont;
8
9/// Resolved layout mode that the renderer applies row-by-row.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LayoutMode {
12    /// No overlap — each glyph occupies its full `max_length` width.
13    FullWidth,
14    /// Kerning — glyphs touch but never smush.
15    Kerning,
16    /// Universal smushing — later char wins (with hardblank dominance).
17    UniversalSmush,
18    /// Smushing using an explicit bitmask of rules 1..=6.
19    RuleSmush(u8),
20    /// Overlap-only — adjacent space cells overlap, nothing else.
21    OverlapOnly,
22}
23
24/// Horizontal justification mode (mirrors [`crate::Justify`] but lives
25/// in the resolver layer).
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Justify {
28    /// Center within the resolved width.
29    Center,
30    /// Left-align.
31    Left,
32    /// Right-align.
33    Right,
34    /// Use the font's print-direction default.
35    FontDefault,
36}
37
38/// One occurrence of a layout-class flag, captured in argv order so
39/// `LayoutResolver::resolve` can apply last-wins semantics per FR-023.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum LayoutFlag {
42    /// `-k`
43    Kerning,
44    /// `-W`
45    FullWidth,
46    /// `-S` — force smush per font's smush rules.
47    ForceSmush,
48    /// `-s` — use the font's default smush.
49    FontDefaultSmush,
50    /// `-o` — overlap only.
51    OverlapOnly,
52    /// `-m N` — explicit bitfield.
53    Explicit(i32),
54}
55
56/// One occurrence of a justify-class flag, captured in argv order.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum JustifyFlag {
59    /// `-c`
60    Center,
61    /// `-l`
62    Left,
63    /// `-r`
64    Right,
65    /// `-x`
66    FontDefault,
67}
68
69/// Sequenced layout-flag occurrences from the command line.
70#[derive(Debug, Clone, Default)]
71pub struct LayoutFlags {
72    /// Occurrences in argv order.
73    pub flags: Vec<LayoutFlag>,
74}
75
76/// Sequenced justify-flag occurrences from the command line.
77#[derive(Debug, Clone, Default)]
78pub struct JustifyFlags {
79    /// Occurrences in argv order.
80    pub flags: Vec<JustifyFlag>,
81}
82
83/// Stateless resolver that collapses a `FIGfont` + flag sequence into a
84/// concrete [`LayoutMode`].
85pub struct LayoutResolver;
86
87impl LayoutResolver {
88    /// Apply last-wins layout-class semantics per AD-009.
89    ///
90    /// When `flags.flags` is empty, returns the font's baseline
91    /// [`LayoutMode`] derived from `full_layout`.
92    pub fn resolve(font: &FIGfont, flags: &LayoutFlags) -> LayoutMode {
93        if let Some(last) = flags.flags.last() {
94            return match *last {
95                LayoutFlag::Kerning => LayoutMode::Kerning,
96                LayoutFlag::FullWidth => LayoutMode::FullWidth,
97                LayoutFlag::ForceSmush => {
98                    let bits = (font.full_layout & 0b0011_1111) as u8;
99                    if bits == 0 {
100                        LayoutMode::UniversalSmush
101                    } else {
102                        LayoutMode::RuleSmush(bits)
103                    }
104                }
105                LayoutFlag::FontDefaultSmush => font_default_mode(font),
106                LayoutFlag::OverlapOnly => LayoutMode::OverlapOnly,
107                LayoutFlag::Explicit(n) => explicit_mode(n),
108            };
109        }
110        font_default_mode(font)
111    }
112}
113
114fn font_default_mode(font: &FIGfont) -> LayoutMode {
115    let smushing = font.full_layout & (crate::smush::RULE_HORIZONTAL_SMUSHING as u32) != 0;
116    let kerning = font.full_layout & (crate::smush::RULE_HORIZONTAL_KERNING as u32) != 0;
117    let bits = (font.full_layout & 0b0011_1111) as u8;
118    if smushing {
119        if bits == 0 {
120            LayoutMode::UniversalSmush
121        } else {
122            LayoutMode::RuleSmush(bits)
123        }
124    } else if kerning {
125        LayoutMode::Kerning
126    } else {
127        LayoutMode::FullWidth
128    }
129}
130
131fn explicit_mode(n: i32) -> LayoutMode {
132    match n {
133        -1 => LayoutMode::FullWidth,
134        0 => LayoutMode::Kerning,
135        // -2 is upstream's "leave layout undefined"; we mirror as font default
136        // when the renderer asks but we lack the font here, so map to kerning.
137        -2 => LayoutMode::Kerning,
138        bits if (1..=63).contains(&bits) => LayoutMode::RuleSmush(bits as u8),
139        _ => LayoutMode::FullWidth,
140    }
141}
142
143/// Collapse a [`JustifyFlags`] sequence into a single [`Justify`] per
144/// FR-022 last-wins semantics. Returns [`Justify::FontDefault`] when
145/// the sequence is empty.
146pub fn resolve_justify(flags: &JustifyFlags) -> Justify {
147    flags
148        .flags
149        .last()
150        .map(|f| match *f {
151            JustifyFlag::Center => Justify::Center,
152            JustifyFlag::Left => Justify::Left,
153            JustifyFlag::Right => Justify::Right,
154            JustifyFlag::FontDefault => Justify::FontDefault,
155        })
156        .unwrap_or(Justify::FontDefault)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::figfont::parse_bytes;
163
164    fn font() -> FIGfont {
165        parse_bytes(crate::figfont::BUNDLED_FONTS[0].1).expect("bundled font parses")
166    }
167
168    #[test]
169    fn empty_flags_yields_font_default() {
170        let mode = LayoutResolver::resolve(&font(), &LayoutFlags::default());
171        let _ = mode; // any of the variants is acceptable for the placeholder
172    }
173
174    #[test]
175    fn last_wins_layout_kerning() {
176        let f = LayoutFlags {
177            flags: vec![
178                LayoutFlag::FullWidth,
179                LayoutFlag::ForceSmush,
180                LayoutFlag::Kerning,
181            ],
182        };
183        assert_eq!(LayoutResolver::resolve(&font(), &f), LayoutMode::Kerning);
184    }
185
186    #[test]
187    fn explicit_layout_bitfield_24() {
188        let f = LayoutFlags {
189            flags: vec![LayoutFlag::Explicit(24)],
190        };
191        assert_eq!(
192            LayoutResolver::resolve(&font(), &f),
193            LayoutMode::RuleSmush(24)
194        );
195    }
196
197    #[test]
198    fn explicit_layout_zero_is_kerning() {
199        let f = LayoutFlags {
200            flags: vec![LayoutFlag::Explicit(0)],
201        };
202        assert_eq!(LayoutResolver::resolve(&font(), &f), LayoutMode::Kerning);
203    }
204
205    #[test]
206    fn justify_last_wins() {
207        let j = JustifyFlags {
208            flags: vec![JustifyFlag::Left, JustifyFlag::Center, JustifyFlag::Right],
209        };
210        assert_eq!(resolve_justify(&j), Justify::Right);
211    }
212
213    #[test]
214    fn justify_empty_yields_font_default() {
215        assert_eq!(
216            resolve_justify(&JustifyFlags::default()),
217            Justify::FontDefault
218        );
219    }
220}