text_fx/presentation.rs
1/// Returns the display width of a string, accounting for Unicode double-width and combining characters.
2///
3/// This function sums the display width of each character in the string, using [`char_display_width`].
4///
5/// # Examples
6///
7/// ```
8/// use text_fx::presentation::display_width;
9///
10/// assert_eq!(display_width("hello"), 5); // ASCII
11/// assert_eq!(display_width("你好"), 4); // CJK
12/// assert_eq!(display_width("á"), 1); // 'a' + combining accent
13/// ```
14#[inline]
15pub fn display_width(s: &str) -> usize {
16 s.chars().map(char_display_width).sum()
17}
18
19/// Returns the display width of a single Unicode character.
20///
21/// - Control and combining characters have width 0.
22/// - CJK and wide symbols have width 2.
23/// - Most other characters have width 1.
24///
25/// # Examples
26///
27/// ```
28/// use text_fx::presentation::char_display_width;
29///
30/// assert_eq!(char_display_width('a'), 1);
31/// assert_eq!(char_display_width('你'), 2);
32/// assert_eq!(char_display_width('\u{0301}'), 0); // combining acute accent
33/// ```
34pub fn char_display_width(c: char) -> usize {
35 use std::cmp::Ordering::*;
36
37 match c {
38 '\u{0000}' => 0, // NULL
39 '\u{0001}'..='\u{001F}' | '\u{007F}' => 0, // Control characters
40
41 '\u{0300}'..='\u{036F}'
42 | '\u{1AB0}'..='\u{1AFF}'
43 | '\u{1DC0}'..='\u{1DFF}'
44 | '\u{20D0}'..='\u{20FF}'
45 | '\u{FE20}'..='\u{FE2F}' => 0, // Combining characters
46
47 _ => match unicode_width_hint(c) {
48 Less => 1,
49 Equal | Greater => 2,
50 },
51 }
52}
53
54/// Heuristically determines if a character is double-width (CJK, emoji, etc).
55///
56/// Returns `Greater` for double-width, `Less` for single-width.
57fn unicode_width_hint(c: char) -> std::cmp::Ordering {
58 match c as u32 {
59 0x1100..=0x115F
60 | 0x2329
61 | 0x232A
62 | 0x2E80..=0xA4CF
63 | 0xAC00..=0xD7A3
64 | 0xF900..=0xFAFF
65 | 0xFE10..=0xFE19
66 | 0xFE30..=0xFE6F
67 | 0xFF00..=0xFF60
68 | 0xFFE0..=0xFFE6
69 | 0x1F300..=0x1F64F
70 | 0x1F900..=0x1F9FF => std::cmp::Ordering::Greater,
71 _ => std::cmp::Ordering::Less,
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn test_display_width() {
81 for (s, expected) in [
82 ("hello", 5), // ASCII → width 5
83 ("你好", 4), // CJK → width 4
84 ("á", 1), // 'a' + combining accent → width 1
85 ] {
86 assert_eq!(display_width(s), expected);
87 }
88 }
89}