1pub fn char_width(c: char, eaw_wide: bool) -> u8 {
32 cp_width(c, eaw_wide)
33}
34
35pub fn grapheme_width(g: &str, eaw_wide: bool) -> u8 {
56 let mut chars = g.chars();
57 let Some(first) = chars.next() else {
58 return 0;
59 };
60 let cp = first as u32;
61
62 if (0x20..0x7f).contains(&cp) {
64 return 1;
65 }
66 if cp < 0x20 || (0x7f..=0x9f).contains(&cp) {
68 return 0;
69 }
70 if is_regional_indicator(cp) {
73 return 2;
74 }
75 if is_default_ignorable(first) {
77 return 0;
78 }
79 if is_extended_pictographic(first) {
82 if g.contains('\u{fe0f}') {
83 return 2;
84 }
85 if g.contains('\u{fe0e}') {
86 return 1;
87 }
88 return if is_emoji_presentation(first) { 2 } else { 1 };
89 }
90 cp_width(first, eaw_wide)
94}
95
96#[cfg(all(not(feature = "icu"), feature = "unicode-rs"))]
101fn cp_width(c: char, eaw_wide: bool) -> u8 {
102 use unicode_width::UnicodeWidthChar;
103 let raw = if eaw_wide {
104 UnicodeWidthChar::width_cjk(c)
105 } else {
106 UnicodeWidthChar::width(c)
107 };
108 raw.unwrap_or(0).min(2) as u8
109}
110
111#[cfg(feature = "icu")]
112fn cp_width(c: char, eaw_wide: bool) -> u8 {
113 use icu_properties::CodePointMapData;
114 use icu_properties::props::{EastAsianWidth, GeneralCategory};
115
116 let cp = c as u32;
117 if cp < 0x20 || (0x7f..=0x9f).contains(&cp) {
118 return 0;
119 }
120 if is_default_ignorable(c) {
121 return 0;
122 }
123 let gc = CodePointMapData::<GeneralCategory>::new().get(c);
124 if matches!(
125 gc,
126 GeneralCategory::NonspacingMark | GeneralCategory::EnclosingMark | GeneralCategory::Format
127 ) {
128 return 0;
129 }
130 match CodePointMapData::<EastAsianWidth>::new().get(c) {
131 EastAsianWidth::Wide | EastAsianWidth::Fullwidth => 2,
132 EastAsianWidth::Ambiguous if eaw_wide => 2,
133 _ => 1,
134 }
135}
136
137#[inline]
142fn is_regional_indicator(cp: u32) -> bool {
143 (0x1f1e6..=0x1f1ff).contains(&cp)
144}
145
146#[cfg(feature = "icu")]
147fn is_default_ignorable(c: char) -> bool {
148 use icu_properties::CodePointSetData;
149 use icu_properties::props::DefaultIgnorableCodePoint;
150 CodePointSetData::new::<DefaultIgnorableCodePoint>().contains(c)
151}
152
153#[cfg(all(not(feature = "icu"), feature = "unicode-rs"))]
154fn is_default_ignorable(c: char) -> bool {
155 matches!(
160 c as u32,
161 0x00ad | 0x034f | 0x061c | 0x115f..=0x1160| 0x17b4..=0x17b5
166 | 0x180b..=0x180f
167 | 0x200b..=0x200f| 0x202a..=0x202e
169 | 0x2060..=0x206f
170 | 0x3164
171 | 0xfe00..=0xfe0f| 0xfeff
173 | 0xffa0
174 | 0xfff0..=0xfff8
175 | 0x1bca0..=0x1bca3
176 | 0x1d173..=0x1d17a
177 | 0xe0000..=0xe0fff
178 )
179}
180
181#[cfg(feature = "icu")]
182fn is_extended_pictographic(c: char) -> bool {
183 use icu_properties::CodePointSetData;
184 use icu_properties::props::ExtendedPictographic;
185 CodePointSetData::new::<ExtendedPictographic>().contains(c)
186}
187
188#[cfg(all(not(feature = "icu"), feature = "unicode-rs"))]
189fn is_extended_pictographic(c: char) -> bool {
190 let cp = c as u32;
193 matches!(
194 cp,
195 0x00a9 | 0x00ae
196 | 0x203c | 0x2049
197 | 0x2122 | 0x2139
198 | 0x2194..=0x2199
199 | 0x21a9..=0x21aa
200 | 0x231a..=0x231b
201 | 0x2328
202 | 0x2388
203 | 0x23cf
204 | 0x23e9..=0x23f3
205 | 0x23f8..=0x23fa
206 | 0x24c2
207 | 0x25aa..=0x25ab
208 | 0x25b6
209 | 0x25c0
210 | 0x25fb..=0x25fe
211 | 0x2600..=0x27bf
212 | 0x2934..=0x2935
213 | 0x2b00..=0x2bff
214 | 0x3030
215 | 0x303d
216 | 0x3297
217 | 0x3299
218 | 0x1f000..=0x1ffff
219 )
220}
221
222#[cfg(feature = "icu")]
223fn is_emoji_presentation(c: char) -> bool {
224 use icu_properties::CodePointSetData;
225 use icu_properties::props::EmojiPresentation;
226 CodePointSetData::new::<EmojiPresentation>().contains(c)
227}
228
229#[cfg(all(not(feature = "icu"), feature = "unicode-rs"))]
230fn is_emoji_presentation(c: char) -> bool {
231 let cp = c as u32;
235 matches!(
236 cp,
237 0x231a..=0x231b
238 | 0x23e9..=0x23ec
239 | 0x23f0
240 | 0x23f3
241 | 0x25fd..=0x25fe
242 | 0x2614..=0x2615
243 | 0x2648..=0x2653
244 | 0x267f
245 | 0x2693
246 | 0x26a1
247 | 0x26aa..=0x26ab
248 | 0x26bd..=0x26be
249 | 0x26c4..=0x26c5
250 | 0x26ce
251 | 0x26d4
252 | 0x26ea
253 | 0x26f2..=0x26f3
254 | 0x26f5
255 | 0x26fa
256 | 0x26fd
257 | 0x2705
258 | 0x270a..=0x270b
259 | 0x2728
260 | 0x274c
261 | 0x274e
262 | 0x2753..=0x2755
263 | 0x2757
264 | 0x2795..=0x2797
265 | 0x27b0
266 | 0x27bf
267 | 0x2b1b..=0x2b1c
268 | 0x2b50
269 | 0x2b55
270 | 0x1f300..=0x1ffff
271 )
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
281 fn ascii_is_one() {
282 for c in [' ', 'a', 'Z', '0', '~'] {
283 assert_eq!(char_width(c, false), 1);
284 assert_eq!(char_width(c, true), 1);
285 }
286 }
287
288 #[test]
289 fn controls_are_zero() {
290 for c in ['\t', '\n', '\r', '\0', '\x1b', '\x7f'] {
291 assert_eq!(char_width(c, false), 0);
292 }
293 }
294
295 #[test]
296 fn cjk_wide_is_two() {
297 assert_eq!(char_width('中', false), 2);
298 assert_eq!(char_width('中', true), 2);
299 assert_eq!(char_width('日', false), 2);
300 }
301
302 #[test]
303 fn east_asian_ambiguous_flag_flips_width() {
304 assert_eq!(char_width('…', false), 1);
306 assert_eq!(char_width('…', true), 2);
307 assert_eq!(char_width('─', false), 1);
309 assert_eq!(char_width('─', true), 2);
310 }
311
312 #[test]
315 fn grapheme_ascii_is_one() {
316 assert_eq!(grapheme_width("A", false), 1);
317 }
318
319 #[test]
320 fn grapheme_cjk_is_two() {
321 assert_eq!(grapheme_width("中", false), 2);
322 }
323
324 #[test]
325 fn flag_emoji_is_two() {
326 assert_eq!(grapheme_width("\u{1f1fa}\u{1f1f8}", false), 2);
328 }
329
330 #[test]
331 fn vs16_promotes_text_glyph_to_emoji() {
332 assert_eq!(grapheme_width("\u{270b}\u{fe0f}", false), 2);
334 }
335
336 #[test]
337 fn vs15_demotes_emoji_to_text() {
338 assert_eq!(grapheme_width("\u{270b}\u{fe0e}", false), 1);
340 }
341
342 #[test]
343 fn grapheme_ambiguous_respects_eaw() {
344 assert_eq!(grapheme_width("…", false), 1);
345 assert_eq!(grapheme_width("…", true), 2);
346 assert_eq!(grapheme_width("─", true), 2);
347 }
348
349 #[test]
350 fn combining_mark_does_not_change_base_width() {
351 assert_eq!(grapheme_width("e\u{0301}", false), 1);
353 }
354}