Skip to main content

matrix_rain/
charset.rs

1//! Built-in glyph sets and the [`CharSet`] enum for supplying your own.
2
3use alloc::format;
4use alloc::vec::Vec;
5
6use crate::error::MatrixError;
7
8/// Source of glyphs for the falling drops.
9///
10/// Each variant resolves to a slice of single-cell characters drawn from
11/// randomly per cell at spawn (and per cell per frame when `mutation_rate > 0`).
12///
13/// # Example
14///
15/// ```
16/// use matrix_rain::{CharSet, MatrixConfig};
17///
18/// let cfg = MatrixConfig::builder()
19///     .charset(CharSet::Hex)
20///     .build()
21///     .unwrap();
22/// assert!(matches!(cfg.charset, CharSet::Hex));
23/// ```
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum CharSet {
26    /// Half-width katakana `U+FF66..=U+FF9D` plus digits `0..=9` (66 glyphs).
27    /// The canonical Matrix look.
28    Matrix,
29    /// Printable ASCII `0x21..=0x7E` (94 glyphs; space is excluded so the
30    /// rain stays visually dense).
31    Ascii,
32    /// Lowercase hexadecimal: `0`–`9` and `a`–`f` (16 glyphs).
33    Hex,
34    /// Just `0` and `1`.
35    Binary,
36    /// User-supplied glyph list. Validated at
37    /// [`MatrixConfigBuilder::build`](crate::MatrixConfigBuilder::build) time:
38    /// must be non-empty and free of `char::is_control` characters. Display
39    /// width is **not** validated — see the crate-level Caveats.
40    Custom(/// Glyphs to draw from.
41        Vec<char>),
42}
43
44impl CharSet {
45    pub(crate) fn chars(&self) -> &[char] {
46        match self {
47            Self::Matrix => MATRIX_CHARS,
48            Self::Ascii => ASCII_CHARS,
49            Self::Hex => HEX_CHARS,
50            Self::Binary => BINARY_CHARS,
51            Self::Custom(v) => v.as_slice(),
52        }
53    }
54
55    pub(crate) fn validate(&self) -> Result<(), MatrixError> {
56        let chars = self.chars();
57        if chars.is_empty() {
58            return Err(MatrixError::EmptyCharset);
59        }
60        for c in chars {
61            if c.is_control() {
62                return Err(MatrixError::InvalidConfig(format!(
63                    "charset contains control character U+{:04X}",
64                    *c as u32
65                )));
66            }
67        }
68        Ok(())
69    }
70}
71
72const MATRIX_CHARS: &[char] = &[
73    'ヲ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ',
74    'ー', 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ',
75    'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ',
76    'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ',
77    'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ',
78    'リ', 'ル', 'レ', 'ロ', 'ワ', 'ン', '0', '1', '2', '3',
79    '4', '5', '6', '7', '8', '9',
80];
81
82const ASCII_CHARS: &[char] = &[
83    '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*',
84    '+', ',', '-', '.', '/', '0', '1', '2', '3', '4',
85    '5', '6', '7', '8', '9', ':', ';', '<', '=', '>',
86    '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
87    'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
88    'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',
89    ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f',
90    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
91    'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
92    '{', '|', '}', '~',
93];
94
95const HEX_CHARS: &[char] = &[
96    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
97];
98
99const BINARY_CHARS: &[char] = &['0', '1'];
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn matrix_chars_non_empty() {
107        assert!(!CharSet::Matrix.chars().is_empty());
108    }
109
110    #[test]
111    fn matrix_chars_include_all_digits() {
112        let chars = CharSet::Matrix.chars();
113        for d in '0'..='9' {
114            assert!(chars.contains(&d), "Matrix charset missing digit {d}");
115        }
116    }
117
118    #[test]
119    fn matrix_chars_include_katakana() {
120        let chars = CharSet::Matrix.chars();
121        assert!(chars.contains(&'ヲ'));
122        assert!(chars.contains(&'ン'));
123    }
124
125    #[test]
126    fn ascii_chars_exclude_space_and_control() {
127        let chars = CharSet::Ascii.chars();
128        assert!(!chars.is_empty());
129        assert!(!chars.contains(&' '));
130        assert!(!chars.contains(&'\n'));
131        assert!(!chars.contains(&'\t'));
132        assert!(chars.contains(&'!'));
133        assert!(chars.contains(&'~'));
134        assert!(chars.contains(&'A'));
135        assert!(chars.contains(&'z'));
136        assert!(chars.contains(&'0'));
137    }
138
139    #[test]
140    fn hex_chars_are_digits_and_lower_af() {
141        let chars = CharSet::Hex.chars();
142        assert_eq!(chars.len(), 16);
143        for d in '0'..='9' {
144            assert!(chars.contains(&d));
145        }
146        for d in 'a'..='f' {
147            assert!(chars.contains(&d));
148        }
149        // No uppercase per spec ("0–9 a–f").
150        assert!(!chars.contains(&'A'));
151    }
152
153    #[test]
154    fn binary_chars_are_zero_and_one() {
155        assert_eq!(CharSet::Binary.chars(), &['0', '1']);
156    }
157
158    #[test]
159    fn custom_passthrough() {
160        let cs = CharSet::Custom(vec!['a', 'b', 'c']);
161        assert_eq!(cs.chars(), &['a', 'b', 'c']);
162    }
163
164    #[test]
165    fn validate_passes_for_all_builtins() {
166        for cs in [CharSet::Matrix, CharSet::Ascii, CharSet::Hex, CharSet::Binary] {
167            assert!(cs.validate().is_ok(), "{cs:?} should validate");
168        }
169    }
170
171    #[test]
172    fn validate_rejects_empty_custom() {
173        let err = CharSet::Custom(vec![]).validate().unwrap_err();
174        assert!(matches!(err, MatrixError::EmptyCharset));
175    }
176
177    #[test]
178    fn validate_rejects_control_chars() {
179        for bad in ['\n', '\r', '\t', '\0', '\x07'] {
180            let err = CharSet::Custom(vec!['a', bad, 'b']).validate().unwrap_err();
181            assert!(
182                matches!(err, MatrixError::InvalidConfig(_)),
183                "control char {bad:?} should be rejected"
184            );
185        }
186    }
187
188    #[test]
189    fn validate_accepts_single_char_custom() {
190        assert!(CharSet::Custom(vec!['x']).validate().is_ok());
191    }
192
193    #[test]
194    fn validate_does_not_check_display_width() {
195        // Full-width / combining chars are NOT detected per spec §5.4 — caller's responsibility.
196        // Just confirm validation passes for one example so the test documents the policy.
197        assert!(CharSet::Custom(vec!['漢']).validate().is_ok());
198    }
199}