Skip to main content

matrix_rain/
charset.rs

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