Skip to main content

presentar_terminal/widgets/
symbols.rs

1//! Braille and block symbol sets for graph rendering.
2//!
3//! Provides 4 distinct symbol sets (Braille, Block, Tty, Custom) with 25 characters
4//! each for btop-style paired-column graph rendering.
5//!
6//! ## SIMD-First Design
7//!
8//! All lookups use direct array indexing O(1) with SIMD-friendly layouts.
9//! The 25-character sets encode two values (0-4 each) in a single character,
10//! allowing 5×5 resolution per cell pair.
11
12#![allow(dead_code)] // Symbol sets may be used in future widgets
13
14/// Braille characters for upward-filling graphs (5×5 grid = 25 chars).
15/// Index: `left_value` * 5 + `right_value` where values are 0-4.
16pub const BRAILLE_UP: [char; 25] = [
17    ' ', '⢀', '⢠', '⢰', '⢸', // left=0, right=0-4
18    '⡀', '⣀', '⣠', '⣰', '⣸', // left=1, right=0-4
19    '⡄', '⣄', '⣤', '⣴', '⣼', // left=2, right=0-4
20    '⡆', '⣆', '⣦', '⣶', '⣾', // left=3, right=0-4
21    '⡇', '⣇', '⣧', '⣷', '⣿', // left=4, right=0-4
22];
23
24/// Braille characters for downward-filling graphs.
25pub const BRAILLE_DOWN: [char; 25] = [
26    ' ', '⠈', '⠘', '⠸', '⢸', // left=0, right=0-4
27    '⠁', '⠉', '⠙', '⠹', '⢹', // left=1, right=0-4
28    '⠃', '⠋', '⠛', '⠻', '⢻', // left=2, right=0-4
29    '⠇', '⠏', '⠟', '⠿', '⢿', // left=3, right=0-4
30    '⡇', '⡏', '⡟', '⡿', '⣿', // left=4, right=0-4
31];
32
33/// Block characters for upward-filling graphs.
34/// Uses half/quarter blocks: ▁▂▃▄▅▆▇█
35pub const BLOCK_UP: [char; 25] = [
36    ' ', '▁', '▂', '▃', '▄', // left=0
37    '▁', '▂', '▃', '▄', '▅', // left=1
38    '▂', '▃', '▄', '▅', '▆', // left=2
39    '▃', '▄', '▅', '▆', '▇', // left=3
40    '▄', '▅', '▆', '▇', '█', // left=4
41];
42
43/// Block characters for downward-filling graphs.
44pub const BLOCK_DOWN: [char; 25] = [
45    ' ', '▔', '▔', '▀', '▀', // left=0
46    '▔', '▔', '▀', '▀', '█', // left=1
47    '▔', '▀', '▀', '█', '█', // left=2
48    '▀', '▀', '█', '█', '█', // left=3
49    '▀', '█', '█', '█', '█', // left=4
50];
51
52/// TTY-safe ASCII characters for graphs (universal compatibility).
53pub const TTY_UP: [char; 25] = [
54    ' ', '.', '.', 'o', 'o', // left=0
55    '.', '.', 'o', 'o', 'O', // left=1
56    '.', 'o', 'o', 'O', 'O', // left=2
57    'o', 'o', 'O', 'O', '#', // left=3
58    'o', 'O', 'O', '#', '#', // left=4
59];
60
61/// TTY-safe ASCII for downward graphs.
62pub const TTY_DOWN: [char; 25] = [
63    ' ', '\'', '\'', '"', '"', // left=0
64    '\'', '\'', '"', '"', '*', // left=1
65    '\'', '"', '"', '*', '*', // left=2
66    '"', '"', '*', '*', '#', // left=3
67    '"', '*', '*', '#', '#', // left=4
68];
69
70/// Single-column sparkline characters (8 levels).
71pub const SPARKLINE: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
72
73/// Superscript digit characters for compact labels.
74pub const SUPERSCRIPT: [char; 10] = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
75
76/// Subscript digit characters.
77pub const SUBSCRIPT: [char; 10] = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
78
79// ============================================================================
80// UX-112: Distinct Category Symbols
81// ============================================================================
82
83/// Category marker symbols - filled circles with varying fill levels.
84/// Use these to distinguish different data categories in legends.
85pub(super) const CATEGORY_FILLED: [char; 6] = ['●', '◐', '◑', '◒', '◓', '○'];
86
87/// Category marker symbols - geometric shapes.
88/// Use for color-blind friendly category distinction.
89pub(super) const CATEGORY_SHAPES: [char; 6] = ['●', '■', '▲', '◆', '★', '◯'];
90
91/// Category marker symbols - checkmarks and status.
92/// Use for pass/fail/warning status indicators.
93pub(super) const CATEGORY_STATUS: [char; 4] = ['✓', '⚠', '✗', '?'];
94
95/// Get a distinct category symbol by index.
96/// Cycles through filled circle variants.
97#[inline]
98pub(super) const fn category_symbol(index: usize) -> char {
99    CATEGORY_FILLED[index % CATEGORY_FILLED.len()]
100}
101
102/// Get a distinct shape symbol by index.
103/// Cycles through geometric shapes for color-blind accessibility.
104#[inline]
105pub(super) const fn shape_symbol(index: usize) -> char {
106    CATEGORY_SHAPES[index % CATEGORY_SHAPES.len()]
107}
108
109/// Arrow symbols for flow diagrams.
110pub(super) const ARROWS_FLOW: [&str; 4] = ["━▶", "──▶", "···▶", "→"];
111
112/// Get consistent arrow style for Sankey diagrams.
113/// UX-113: Standardized arrow styles based on flow strength.
114#[inline]
115pub(super) const fn flow_arrow(strength: usize) -> &'static str {
116    // Use if-else instead of .min() for const fn compatibility
117    let idx = if strength > 3 { 3 } else { strength };
118    ARROWS_FLOW[idx]
119}
120
121/// Symbol set variants for graph rendering.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub enum SymbolSet {
124    /// Braille patterns - highest resolution (2×4 dots per cell).
125    #[default]
126    Braille,
127    /// Block characters - high compatibility.
128    Block,
129    /// TTY-safe ASCII - universal compatibility.
130    Tty,
131    /// Custom user-defined set.
132    Custom,
133}
134
135/// Custom symbol set with user-defined characters.
136#[derive(Debug, Clone)]
137pub struct CustomSymbols {
138    /// Upward-filling characters (25).
139    pub up: [char; 25],
140    /// Downward-filling characters (25).
141    pub down: [char; 25],
142}
143
144impl Default for CustomSymbols {
145    fn default() -> Self {
146        Self {
147            up: BRAILLE_UP,
148            down: BRAILLE_DOWN,
149        }
150    }
151}
152
153impl CustomSymbols {
154    /// Create custom symbols from a string of 50 characters.
155    /// First 25 are up, next 25 are down.
156    #[must_use]
157    pub fn from_chars(chars: &str) -> Option<Self> {
158        let chars: Vec<char> = chars.chars().collect();
159        if chars.len() < 50 {
160            return None;
161        }
162        let mut up = [' '; 25];
163        let mut down = [' '; 25];
164        up.copy_from_slice(&chars[0..25]);
165        down.copy_from_slice(&chars[25..50]);
166        Some(Self { up, down })
167    }
168}
169
170/// Unified symbol interface for graph rendering.
171#[derive(Debug, Clone)]
172pub struct BrailleSymbols {
173    set: SymbolSet,
174    custom: Option<CustomSymbols>,
175}
176
177impl Default for BrailleSymbols {
178    fn default() -> Self {
179        Self::new(SymbolSet::default())
180    }
181}
182
183impl BrailleSymbols {
184    /// Create symbols with specified set.
185    #[must_use]
186    pub fn new(set: SymbolSet) -> Self {
187        Self { set, custom: None }
188    }
189
190    /// Create symbols with custom character set.
191    #[must_use]
192    pub fn with_custom(custom: CustomSymbols) -> Self {
193        Self {
194            set: SymbolSet::Custom,
195            custom: Some(custom),
196        }
197    }
198
199    /// Get the current symbol set type.
200    #[must_use]
201    pub fn set(&self) -> SymbolSet {
202        self.set
203    }
204
205    /// Get the up-direction symbol array.
206    #[must_use]
207    pub fn up_chars(&self) -> &[char; 25] {
208        match self.set {
209            SymbolSet::Braille => &BRAILLE_UP,
210            SymbolSet::Block => &BLOCK_UP,
211            SymbolSet::Tty => &TTY_UP,
212            SymbolSet::Custom => self.custom.as_ref().map_or(&BRAILLE_UP, |c| &c.up),
213        }
214    }
215
216    /// Get the down-direction symbol array.
217    #[must_use]
218    pub fn down_chars(&self) -> &[char; 25] {
219        match self.set {
220            SymbolSet::Braille => &BRAILLE_DOWN,
221            SymbolSet::Block => &BLOCK_DOWN,
222            SymbolSet::Tty => &TTY_DOWN,
223            SymbolSet::Custom => self.custom.as_ref().map_or(&BRAILLE_DOWN, |c| &c.down),
224        }
225    }
226
227    /// Get character for value (0.0-1.0) in up direction.
228    /// Maps to single-column (uses left value only, right=0).
229    #[inline]
230    #[must_use]
231    pub fn char_up(&self, value: f64) -> char {
232        let level = (value.clamp(0.0, 1.0) * 4.0).round() as usize;
233        self.up_chars()[level.min(4) * 5] // left=level, right=0
234    }
235
236    /// Get character for value in down direction.
237    #[inline]
238    #[must_use]
239    pub fn char_down(&self, value: f64) -> char {
240        let level = (value.clamp(0.0, 1.0) * 4.0).round() as usize;
241        self.down_chars()[level.min(4) * 5]
242    }
243
244    /// Get character for paired values (left 0-4, right 0-4).
245    /// SIMD-friendly: direct array lookup with index = left * 5 + right.
246    #[inline]
247    #[must_use]
248    pub fn char_pair(&self, left: u8, right: u8) -> char {
249        let idx = (left.min(4) as usize) * 5 + (right.min(4) as usize);
250        self.up_chars()[idx]
251    }
252
253    /// Get character for paired values in down direction.
254    #[inline]
255    #[must_use]
256    pub fn char_pair_down(&self, left: u8, right: u8) -> char {
257        let idx = (left.min(4) as usize) * 5 + (right.min(4) as usize);
258        self.down_chars()[idx]
259    }
260
261    /// Get sparkline character for value (0.0-1.0).
262    /// Uses 8-level sparkline characters.
263    #[inline]
264    #[must_use]
265    pub fn sparkline_char(value: f64) -> char {
266        let level = (value.clamp(0.0, 1.0) * 7.0).round() as usize;
267        SPARKLINE[level.min(7)]
268    }
269
270    /// Convert number to superscript string.
271    #[must_use]
272    pub fn to_superscript(n: u32) -> String {
273        n.to_string()
274            .chars()
275            .filter_map(|c| c.to_digit(10).map(|d| SUPERSCRIPT[d as usize]))
276            .collect()
277    }
278
279    /// Convert number to subscript string.
280    #[must_use]
281    pub fn to_subscript(n: u32) -> String {
282        n.to_string()
283            .chars()
284            .filter_map(|c| c.to_digit(10).map(|d| SUBSCRIPT[d as usize]))
285            .collect()
286    }
287}
288
289#[cfg(test)]
290#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
291mod tests {
292    use super::*;
293
294    // =====================================================
295    // Symbol Set Array Tests
296    // =====================================================
297
298    #[test]
299    fn test_braille_up_array_length() {
300        assert_eq!(BRAILLE_UP.len(), 25);
301    }
302
303    #[test]
304    fn test_braille_down_array_length() {
305        assert_eq!(BRAILLE_DOWN.len(), 25);
306    }
307
308    #[test]
309    fn test_block_up_array_length() {
310        assert_eq!(BLOCK_UP.len(), 25);
311    }
312
313    #[test]
314    fn test_block_down_array_length() {
315        assert_eq!(BLOCK_DOWN.len(), 25);
316    }
317
318    #[test]
319    fn test_tty_up_array_length() {
320        assert_eq!(TTY_UP.len(), 25);
321    }
322
323    #[test]
324    fn test_tty_down_array_length() {
325        assert_eq!(TTY_DOWN.len(), 25);
326    }
327
328    #[test]
329    fn test_sparkline_array_length() {
330        assert_eq!(SPARKLINE.len(), 8);
331    }
332
333    #[test]
334    fn test_superscript_array_length() {
335        assert_eq!(SUPERSCRIPT.len(), 10);
336    }
337
338    #[test]
339    fn test_subscript_array_length() {
340        assert_eq!(SUBSCRIPT.len(), 10);
341    }
342
343    // =====================================================
344    // Braille Symbol Specific Tests
345    // =====================================================
346
347    #[test]
348    fn test_braille_up_empty_is_space() {
349        assert_eq!(BRAILLE_UP[0], ' '); // left=0, right=0
350    }
351
352    #[test]
353    fn test_braille_up_full_is_full() {
354        assert_eq!(BRAILLE_UP[24], '⣿'); // left=4, right=4
355    }
356
357    #[test]
358    fn test_braille_up_left_only() {
359        assert_eq!(BRAILLE_UP[5], '⡀'); // left=1, right=0
360        assert_eq!(BRAILLE_UP[10], '⡄'); // left=2, right=0
361        assert_eq!(BRAILLE_UP[15], '⡆'); // left=3, right=0
362        assert_eq!(BRAILLE_UP[20], '⡇'); // left=4, right=0
363    }
364
365    #[test]
366    fn test_braille_up_right_only() {
367        assert_eq!(BRAILLE_UP[1], '⢀'); // left=0, right=1
368        assert_eq!(BRAILLE_UP[2], '⢠'); // left=0, right=2
369        assert_eq!(BRAILLE_UP[3], '⢰'); // left=0, right=3
370        assert_eq!(BRAILLE_UP[4], '⢸'); // left=0, right=4
371    }
372
373    #[test]
374    fn test_braille_down_empty_is_space() {
375        assert_eq!(BRAILLE_DOWN[0], ' ');
376    }
377
378    #[test]
379    fn test_braille_down_full_is_full() {
380        assert_eq!(BRAILLE_DOWN[24], '⣿');
381    }
382
383    // =====================================================
384    // Block Symbol Tests
385    // =====================================================
386
387    #[test]
388    fn test_block_up_empty_is_space() {
389        assert_eq!(BLOCK_UP[0], ' ');
390    }
391
392    #[test]
393    fn test_block_up_full_is_full_block() {
394        assert_eq!(BLOCK_UP[24], '█');
395    }
396
397    #[test]
398    fn test_block_up_progression() {
399        // First row should progress: space, ▁, ▂, ▃, ▄
400        assert_eq!(BLOCK_UP[0], ' ');
401        assert_eq!(BLOCK_UP[1], '▁');
402        assert_eq!(BLOCK_UP[2], '▂');
403        assert_eq!(BLOCK_UP[3], '▃');
404        assert_eq!(BLOCK_UP[4], '▄');
405    }
406
407    // =====================================================
408    // TTY Symbol Tests
409    // =====================================================
410
411    #[test]
412    fn test_tty_up_empty_is_space() {
413        assert_eq!(TTY_UP[0], ' ');
414    }
415
416    #[test]
417    fn test_tty_up_uses_ascii_only() {
418        for c in TTY_UP.iter() {
419            assert!(c.is_ascii() || *c == ' ', "Non-ASCII char: {}", c);
420        }
421    }
422
423    #[test]
424    fn test_tty_down_uses_ascii_only() {
425        for c in TTY_DOWN.iter() {
426            assert!(c.is_ascii() || *c == ' ', "Non-ASCII char: {}", c);
427        }
428    }
429
430    // =====================================================
431    // SymbolSet Enum Tests
432    // =====================================================
433
434    #[test]
435    fn test_symbol_set_default_is_braille() {
436        assert_eq!(SymbolSet::default(), SymbolSet::Braille);
437    }
438
439    #[test]
440    fn test_symbol_set_variants() {
441        let _ = SymbolSet::Braille;
442        let _ = SymbolSet::Block;
443        let _ = SymbolSet::Tty;
444        let _ = SymbolSet::Custom;
445    }
446
447    // =====================================================
448    // CustomSymbols Tests
449    // =====================================================
450
451    #[test]
452    fn test_custom_symbols_default() {
453        let custom = CustomSymbols::default();
454        assert_eq!(custom.up, BRAILLE_UP);
455        assert_eq!(custom.down, BRAILLE_DOWN);
456    }
457
458    #[test]
459    fn test_custom_symbols_from_chars_valid() {
460        let chars: String = std::iter::repeat_n('X', 50).collect();
461        let custom = CustomSymbols::from_chars(&chars);
462        assert!(custom.is_some());
463        let custom = custom.unwrap();
464        assert_eq!(custom.up[0], 'X');
465        assert_eq!(custom.down[0], 'X');
466    }
467
468    #[test]
469    fn test_custom_symbols_from_chars_too_short() {
470        let custom = CustomSymbols::from_chars("short");
471        assert!(custom.is_none());
472    }
473
474    #[test]
475    fn test_custom_symbols_from_chars_49_chars() {
476        let chars: String = std::iter::repeat_n('X', 49).collect();
477        let custom = CustomSymbols::from_chars(&chars);
478        assert!(custom.is_none());
479    }
480
481    // =====================================================
482    // BrailleSymbols Construction Tests
483    // =====================================================
484
485    #[test]
486    fn test_braille_symbols_new_braille() {
487        let sym = BrailleSymbols::new(SymbolSet::Braille);
488        assert_eq!(sym.set(), SymbolSet::Braille);
489    }
490
491    #[test]
492    fn test_braille_symbols_new_block() {
493        let sym = BrailleSymbols::new(SymbolSet::Block);
494        assert_eq!(sym.set(), SymbolSet::Block);
495    }
496
497    #[test]
498    fn test_braille_symbols_new_tty() {
499        let sym = BrailleSymbols::new(SymbolSet::Tty);
500        assert_eq!(sym.set(), SymbolSet::Tty);
501    }
502
503    #[test]
504    fn test_braille_symbols_default() {
505        let sym = BrailleSymbols::default();
506        assert_eq!(sym.set(), SymbolSet::Braille);
507    }
508
509    #[test]
510    fn test_braille_symbols_with_custom() {
511        let custom = CustomSymbols::default();
512        let sym = BrailleSymbols::with_custom(custom);
513        assert_eq!(sym.set(), SymbolSet::Custom);
514    }
515
516    // =====================================================
517    // Up/Down Chars Selection Tests
518    // =====================================================
519
520    #[test]
521    fn test_up_chars_braille() {
522        let sym = BrailleSymbols::new(SymbolSet::Braille);
523        assert_eq!(sym.up_chars(), &BRAILLE_UP);
524    }
525
526    #[test]
527    fn test_up_chars_block() {
528        let sym = BrailleSymbols::new(SymbolSet::Block);
529        assert_eq!(sym.up_chars(), &BLOCK_UP);
530    }
531
532    #[test]
533    fn test_up_chars_tty() {
534        let sym = BrailleSymbols::new(SymbolSet::Tty);
535        assert_eq!(sym.up_chars(), &TTY_UP);
536    }
537
538    #[test]
539    fn test_up_chars_custom() {
540        let mut custom = CustomSymbols::default();
541        custom.up[0] = 'A';
542        let sym = BrailleSymbols::with_custom(custom);
543        assert_eq!(sym.up_chars()[0], 'A');
544    }
545
546    #[test]
547    fn test_down_chars_braille() {
548        let sym = BrailleSymbols::new(SymbolSet::Braille);
549        assert_eq!(sym.down_chars(), &BRAILLE_DOWN);
550    }
551
552    #[test]
553    fn test_down_chars_block() {
554        let sym = BrailleSymbols::new(SymbolSet::Block);
555        assert_eq!(sym.down_chars(), &BLOCK_DOWN);
556    }
557
558    #[test]
559    fn test_down_chars_tty() {
560        let sym = BrailleSymbols::new(SymbolSet::Tty);
561        assert_eq!(sym.down_chars(), &TTY_DOWN);
562    }
563
564    // =====================================================
565    // char_up Tests
566    // =====================================================
567
568    #[test]
569    fn test_char_up_zero() {
570        let sym = BrailleSymbols::new(SymbolSet::Braille);
571        assert_eq!(sym.char_up(0.0), ' ');
572    }
573
574    #[test]
575    fn test_char_up_one() {
576        let sym = BrailleSymbols::new(SymbolSet::Braille);
577        assert_eq!(sym.char_up(1.0), '⡇'); // left=4, right=0
578    }
579
580    #[test]
581    fn test_char_up_half() {
582        let sym = BrailleSymbols::new(SymbolSet::Braille);
583        let c = sym.char_up(0.5);
584        assert_eq!(c, '⡄'); // left=2, right=0
585    }
586
587    #[test]
588    fn test_char_up_clamps_negative() {
589        let sym = BrailleSymbols::new(SymbolSet::Braille);
590        assert_eq!(sym.char_up(-1.0), ' ');
591    }
592
593    #[test]
594    fn test_char_up_clamps_over_one() {
595        let sym = BrailleSymbols::new(SymbolSet::Braille);
596        assert_eq!(sym.char_up(2.0), '⡇');
597    }
598
599    // =====================================================
600    // char_down Tests
601    // =====================================================
602
603    #[test]
604    fn test_char_down_zero() {
605        let sym = BrailleSymbols::new(SymbolSet::Braille);
606        assert_eq!(sym.char_down(0.0), ' ');
607    }
608
609    #[test]
610    fn test_char_down_one() {
611        let sym = BrailleSymbols::new(SymbolSet::Braille);
612        assert_eq!(sym.char_down(1.0), '⡇');
613    }
614
615    // =====================================================
616    // char_pair Tests
617    // =====================================================
618
619    #[test]
620    fn test_char_pair_zero_zero() {
621        let sym = BrailleSymbols::new(SymbolSet::Braille);
622        assert_eq!(sym.char_pair(0, 0), ' ');
623    }
624
625    #[test]
626    fn test_char_pair_four_four() {
627        let sym = BrailleSymbols::new(SymbolSet::Braille);
628        assert_eq!(sym.char_pair(4, 4), '⣿');
629    }
630
631    #[test]
632    fn test_char_pair_left_only() {
633        let sym = BrailleSymbols::new(SymbolSet::Braille);
634        assert_eq!(sym.char_pair(2, 0), '⡄');
635    }
636
637    #[test]
638    fn test_char_pair_right_only() {
639        let sym = BrailleSymbols::new(SymbolSet::Braille);
640        assert_eq!(sym.char_pair(0, 2), '⢠');
641    }
642
643    #[test]
644    fn test_char_pair_clamps_left() {
645        let sym = BrailleSymbols::new(SymbolSet::Braille);
646        assert_eq!(sym.char_pair(10, 0), '⡇'); // 10 clamped to 4
647    }
648
649    #[test]
650    fn test_char_pair_clamps_right() {
651        let sym = BrailleSymbols::new(SymbolSet::Braille);
652        assert_eq!(sym.char_pair(0, 10), '⢸'); // 10 clamped to 4
653    }
654
655    #[test]
656    fn test_char_pair_down() {
657        let sym = BrailleSymbols::new(SymbolSet::Braille);
658        assert_eq!(sym.char_pair_down(0, 0), ' ');
659        assert_eq!(sym.char_pair_down(4, 4), '⣿');
660    }
661
662    // =====================================================
663    // Sparkline Tests
664    // =====================================================
665
666    #[test]
667    fn test_sparkline_char_zero() {
668        assert_eq!(BrailleSymbols::sparkline_char(0.0), '▁');
669    }
670
671    #[test]
672    fn test_sparkline_char_one() {
673        assert_eq!(BrailleSymbols::sparkline_char(1.0), '█');
674    }
675
676    #[test]
677    fn test_sparkline_char_half() {
678        // 0.5 * 7 = 3.5, rounds to 4, which is '▅' (index 4)
679        let c = BrailleSymbols::sparkline_char(0.5);
680        assert_eq!(c, '▅');
681    }
682
683    #[test]
684    fn test_sparkline_char_clamps() {
685        assert_eq!(BrailleSymbols::sparkline_char(-0.5), '▁');
686        assert_eq!(BrailleSymbols::sparkline_char(1.5), '█');
687    }
688
689    // =====================================================
690    // Superscript/Subscript Tests
691    // =====================================================
692
693    #[test]
694    fn test_to_superscript_single_digit() {
695        assert_eq!(BrailleSymbols::to_superscript(5), "⁵");
696    }
697
698    #[test]
699    fn test_to_superscript_multi_digit() {
700        assert_eq!(BrailleSymbols::to_superscript(123), "¹²³");
701    }
702
703    #[test]
704    fn test_to_superscript_zero() {
705        assert_eq!(BrailleSymbols::to_superscript(0), "⁰");
706    }
707
708    #[test]
709    fn test_to_subscript_single_digit() {
710        assert_eq!(BrailleSymbols::to_subscript(5), "₅");
711    }
712
713    #[test]
714    fn test_to_subscript_multi_digit() {
715        assert_eq!(BrailleSymbols::to_subscript(123), "₁₂₃");
716    }
717
718    #[test]
719    fn test_to_subscript_zero() {
720        assert_eq!(BrailleSymbols::to_subscript(0), "₀");
721    }
722
723    #[test]
724    fn test_superscript_all_digits() {
725        let result = BrailleSymbols::to_superscript(1234567890);
726        assert_eq!(result, "¹²³⁴⁵⁶⁷⁸⁹⁰");
727    }
728
729    #[test]
730    fn test_subscript_all_digits() {
731        let result = BrailleSymbols::to_subscript(1234567890);
732        assert_eq!(result, "₁₂₃₄₅₆₇₈₉₀");
733    }
734
735    // =====================================================
736    // Index Calculation Tests (SIMD-friendly verification)
737    // =====================================================
738
739    #[test]
740    fn test_index_calculation() {
741        // Verify left * 5 + right formula
742        for left in 0..5u8 {
743            for right in 0..5u8 {
744                let idx = (left as usize) * 5 + (right as usize);
745                assert!(idx < 25, "Index out of bounds: {}", idx);
746            }
747        }
748    }
749
750    #[test]
751    fn test_all_braille_up_unique() {
752        let mut seen = std::collections::HashSet::new();
753        for &c in BRAILLE_UP.iter() {
754            // Space appears only once at index 0
755            if c != ' ' {
756                assert!(seen.insert(c), "Duplicate character: {}", c);
757            }
758        }
759    }
760
761    #[test]
762    fn test_all_braille_down_unique() {
763        let mut seen = std::collections::HashSet::new();
764        for &c in BRAILLE_DOWN.iter() {
765            if c != ' ' {
766                assert!(seen.insert(c), "Duplicate character: {}", c);
767            }
768        }
769    }
770
771    // =====================================================
772    // Block Mode Tests
773    // =====================================================
774
775    #[test]
776    fn test_block_char_up() {
777        let sym = BrailleSymbols::new(SymbolSet::Block);
778        assert_eq!(sym.char_up(0.0), ' ');
779        assert_eq!(sym.char_up(1.0), '▄'); // left=4, right=0 in BLOCK_UP
780    }
781
782    #[test]
783    fn test_block_char_pair() {
784        let sym = BrailleSymbols::new(SymbolSet::Block);
785        assert_eq!(sym.char_pair(0, 0), ' ');
786        assert_eq!(sym.char_pair(4, 4), '█');
787    }
788
789    // =====================================================
790    // TTY Mode Tests
791    // =====================================================
792
793    #[test]
794    fn test_tty_char_up() {
795        let sym = BrailleSymbols::new(SymbolSet::Tty);
796        assert_eq!(sym.char_up(0.0), ' ');
797        assert_eq!(sym.char_up(1.0), 'o'); // left=4, right=0 in TTY_UP
798    }
799
800    #[test]
801    fn test_tty_char_pair() {
802        let sym = BrailleSymbols::new(SymbolSet::Tty);
803        assert_eq!(sym.char_pair(0, 0), ' ');
804        assert_eq!(sym.char_pair(4, 4), '#');
805    }
806
807    // =====================================================
808    // Custom Mode Fallback Tests
809    // =====================================================
810
811    #[test]
812    fn test_custom_without_data_uses_braille() {
813        // Create a BrailleSymbols with Custom set but no custom data
814        let sym = BrailleSymbols {
815            set: SymbolSet::Custom,
816            custom: None,
817        };
818        // Should fallback to BRAILLE_UP
819        assert_eq!(sym.up_chars(), &BRAILLE_UP);
820        assert_eq!(sym.down_chars(), &BRAILLE_DOWN);
821    }
822}