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}