Skip to main content

fresh_core/
display_width.rs

1//! Display width calculation for Unicode text.
2//!
3//! The single source of truth for "how many terminal columns does this text
4//! occupy", backed by the `unicode-width` crate. Used for cursor positioning,
5//! line wrapping, and UI layout with CJK characters, emoji, and other
6//! double-width or zero-width characters.
7//!
8//! This lives in `fresh-core` so that both the editor (layout/rendering) and
9//! the plugin runtime (the `charWidth` / `stringWidth` plugin APIs) compute
10//! width with the *same* logic — plugins must not re-derive their own width
11//! tables, or their measurements drift from how the editor actually lays out
12//! cells.
13
14use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
15
16/// Display width of a single character, in terminal columns.
17///
18/// Returns 0 for control and zero-width characters, 2 for CJK/fullwidth
19/// characters and most emoji, and 1 for everything else.
20#[inline]
21pub fn char_width(c: char) -> usize {
22    // unicode_width returns None for control characters.
23    c.width().unwrap_or(0)
24}
25
26/// Display width of a string, in terminal columns (the sum of its characters'
27/// widths). Use this instead of `.chars().count()` for visual layout.
28#[inline]
29pub fn str_width(s: &str) -> usize {
30    s.width()
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    #[test]
38    fn ascii() {
39        assert_eq!(str_width("Hello"), 5);
40        assert_eq!(str_width(""), 0);
41        assert_eq!(char_width('a'), 1);
42    }
43
44    #[test]
45    fn cjk_and_emoji_are_two_columns() {
46        assert_eq!(char_width('你'), 2);
47        assert_eq!(char_width('🚀'), 2);
48        assert_eq!(str_width("你好"), 4);
49        assert_eq!(str_width("Hi🚀"), 4);
50    }
51
52    #[test]
53    fn control_and_zero_width_are_zero() {
54        assert_eq!(char_width('\0'), 0);
55        assert_eq!(char_width('\t'), 0);
56        assert_eq!(char_width('\u{200B}'), 0); // zero-width space
57    }
58}