opentui_rust/unicode/
width.rs1use std::collections::HashMap;
4use std::sync::atomic::{AtomicU8, Ordering};
5use std::sync::{OnceLock, RwLock};
6use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum WidthMethod {
11 #[default]
13 WcWidth,
14 Unicode,
16}
17
18const WIDTH_METHOD_WCWIDTH: u8 = 0;
19const WIDTH_METHOD_UNICODE: u8 = 1;
20
21static WIDTH_METHOD: AtomicU8 = AtomicU8::new(WIDTH_METHOD_WCWIDTH);
22
23static WIDTH_OVERRIDES: OnceLock<RwLock<HashMap<char, usize>>> = OnceLock::new();
24static WIDTH_OVERRIDES_ENABLED: std::sync::atomic::AtomicBool =
25 std::sync::atomic::AtomicBool::new(false);
26
27fn width_overrides() -> &'static RwLock<HashMap<char, usize>> {
28 WIDTH_OVERRIDES.get_or_init(|| RwLock::new(HashMap::new()))
29}
30
31pub fn set_width_override(ch: char, width: usize) {
33 {
34 let mut map = width_overrides()
35 .write()
36 .expect("width override lock poisoned");
37 map.insert(ch, width);
38 }
39 WIDTH_OVERRIDES_ENABLED.store(true, Ordering::Release);
40}
41
42#[must_use]
44pub fn get_width_override(ch: char) -> Option<usize> {
45 if !WIDTH_OVERRIDES_ENABLED.load(Ordering::Acquire) {
46 return None;
47 }
48
49 let map = WIDTH_OVERRIDES
50 .get()?
51 .read()
52 .expect("width override lock poisoned");
53 map.get(&ch).copied()
54}
55
56pub fn clear_width_overrides() {
58 if let Some(map) = WIDTH_OVERRIDES.get() {
59 map.write().expect("width override lock poisoned").clear();
60 }
61 WIDTH_OVERRIDES_ENABLED.store(false, Ordering::Release);
62}
63
64pub fn set_width_method(method: WidthMethod) {
66 let value = match method {
67 WidthMethod::WcWidth => WIDTH_METHOD_WCWIDTH,
68 WidthMethod::Unicode => WIDTH_METHOD_UNICODE,
69 };
70 WIDTH_METHOD.store(value, Ordering::Relaxed);
71}
72
73#[must_use]
75pub fn width_method() -> WidthMethod {
76 match WIDTH_METHOD.load(Ordering::Relaxed) {
77 WIDTH_METHOD_UNICODE => WidthMethod::Unicode,
78 _ => WidthMethod::WcWidth,
79 }
80}
81
82#[must_use]
84pub fn display_width(s: &str) -> usize {
85 display_width_with_method(s, width_method())
86}
87
88#[inline]
93#[must_use]
94pub fn display_width_char(c: char) -> usize {
95 if c.is_ascii() && (' '..='~').contains(&c) {
98 return 1;
99 }
100 if c < ' ' {
102 return 0;
103 }
104 display_width_char_with_method(c, width_method())
105}
106
107#[must_use]
109pub fn display_width_with_method(s: &str, method: WidthMethod) -> usize {
110 if WIDTH_OVERRIDES_ENABLED.load(Ordering::Acquire) {
111 return s
112 .chars()
113 .map(|ch| display_width_char_with_method(ch, method))
114 .sum();
115 }
116
117 match method {
118 WidthMethod::WcWidth => UnicodeWidthStr::width(s),
119 WidthMethod::Unicode => UnicodeWidthStr::width_cjk(s),
120 }
121}
122
123#[must_use]
125pub fn display_width_char_with_method(c: char, method: WidthMethod) -> usize {
126 if let Some(width) = get_width_override(c) {
127 return width;
128 }
129
130 match method {
131 WidthMethod::WcWidth => UnicodeWidthChar::width(c).unwrap_or(0),
132 WidthMethod::Unicode => UnicodeWidthChar::width_cjk(c).unwrap_or(0),
133 }
134}
135
136#[must_use]
138pub fn is_zero_width(c: char) -> bool {
139 display_width_char(c) == 0
140}
141
142#[must_use]
144pub fn is_wide(c: char) -> bool {
145 display_width_char(c) == 2
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 struct ClearOverridesOnDrop;
153
154 impl Drop for ClearOverridesOnDrop {
155 fn drop(&mut self) {
156 clear_width_overrides();
157 }
158 }
159
160 #[test]
161 fn test_ascii_width() {
162 assert_eq!(display_width("hello"), 5);
163 assert_eq!(display_width_char('a'), 1);
164 }
165
166 #[test]
167 fn test_cjk_width() {
168 assert_eq!(display_width("æ¼¢å—"), 4);
169 assert_eq!(display_width_char('æ¼¢'), 2);
170 assert!(is_wide('æ¼¢'));
171 }
172
173 #[test]
174 fn test_emoji_width() {
175 assert_eq!(display_width("😀"), 2);
177 }
178
179 #[test]
180 fn test_zero_width() {
181 assert!(is_zero_width('\u{0301}')); }
184
185 #[test]
186 fn test_width_methods() {
187 let ch = 'â‘ ';
190 assert_eq!(display_width_char_with_method(ch, WidthMethod::WcWidth), 1);
191 assert_eq!(display_width_char_with_method(ch, WidthMethod::Unicode), 2);
192 }
193
194 #[test]
195 fn test_width_overrides_set_get_clear() {
196 let _guard = ClearOverridesOnDrop;
197
198 assert_eq!(get_width_override('🦀'), None);
199
200 set_width_override('🦀', 1);
201 assert_eq!(get_width_override('🦀'), Some(1));
202
203 clear_width_overrides();
204 assert_eq!(get_width_override('🦀'), None);
205 }
206
207 #[test]
208 fn test_width_calculation_uses_override() {
209 let _guard = ClearOverridesOnDrop;
210
211 assert_eq!(display_width_char('🦀'), 2);
213
214 set_width_override('🦀', 1);
215 assert_eq!(display_width_char('🦀'), 1);
216 assert_eq!(display_width("A🦀B"), 3);
217 }
218}