1use crate::style::Style;
4use unicode_width::UnicodeWidthStr;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Cell {
9 pub grapheme: String,
11 pub style: Style,
13 pub width: u8,
15}
16
17impl Cell {
18 pub fn new(grapheme: impl Into<String>, style: Style) -> Self {
20 let grapheme = grapheme.into();
21 let width = UnicodeWidthStr::width(grapheme.as_str()) as u8;
22 Self {
23 grapheme,
24 style,
25 width,
26 }
27 }
28
29 pub fn blank() -> Self {
31 Self {
32 grapheme: " ".into(),
33 style: Style::default(),
34 width: 1,
35 }
36 }
37
38 pub fn is_blank(&self) -> bool {
40 self.grapheme == " " && self.style.is_empty() && self.width == 1
41 }
42
43 pub fn is_wide(&self) -> bool {
45 self.width > 1
46 }
47
48 pub fn is_continuation(&self) -> bool {
52 self.width == 0
53 }
54
55 pub fn continuation() -> Self {
57 Self {
58 grapheme: String::new(),
59 style: Style::default(),
60 width: 0,
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::color::{Color, NamedColor};
69
70 #[test]
71 fn blank_cell() {
72 let c = Cell::blank();
73 assert!(c.is_blank());
74 assert_eq!(c.width, 1);
75 }
76
77 #[test]
78 fn ascii_cell() {
79 let c = Cell::new("A", Style::default());
80 assert_eq!(c.width, 1);
81 assert!(!c.is_wide());
82 }
83
84 #[test]
85 fn cjk_cell() {
86 let c = Cell::new("\u{4e16}", Style::default()); assert_eq!(c.width, 2);
88 assert!(c.is_wide());
89 }
90
91 #[test]
92 fn continuation_cell() {
93 let c = Cell::continuation();
94 assert_eq!(c.width, 0);
95 assert!(c.grapheme.is_empty());
96 }
97
98 #[test]
99 fn styled_not_blank() {
100 let c = Cell::new(" ", Style::new().fg(Color::Named(NamedColor::Red)));
101 assert!(!c.is_blank());
102 }
103
104 #[test]
105 fn space_default_is_blank() {
106 let c = Cell::new(" ", Style::default());
107 assert!(c.is_blank());
108 }
109
110 #[test]
113 fn cell_from_emoji_width_two() {
114 let c = Cell::new("\u{1f389}", Style::default()); assert_eq!(c.width, 2);
116 assert!(c.is_wide());
117 }
118
119 #[test]
120 fn cell_from_combining_mark_width_zero() {
121 let c = Cell::new("\u{0301}", Style::default());
123 assert_eq!(c.width, 0);
124 }
125
126 #[test]
127 fn cell_from_cjk_width_two() {
128 let c = Cell::new("\u{6f22}", Style::default()); assert_eq!(c.width, 2);
130 assert!(c.is_wide());
131 }
132
133 #[test]
134 fn cell_from_ascii_width_one() {
135 let c = Cell::new("A", Style::default());
136 assert_eq!(c.width, 1);
137 assert!(!c.is_wide());
138 }
139
140 #[test]
141 fn cell_equality_same_grapheme_and_style() {
142 let style = Style::new().fg(Color::Named(NamedColor::Green));
143 let c1 = Cell::new("X", style.clone());
144 let c2 = Cell::new("X", style);
145 assert_eq!(c1, c2);
146 }
147
148 #[test]
149 fn cell_inequality_different_width() {
150 let c1 = Cell::new("A", Style::default());
152 let c2 = Cell::new("\u{4e16}", Style::default());
153 assert_ne!(c1, c2);
154 assert_ne!(c1.width, c2.width);
155 }
156
157 #[test]
160 fn cell_from_zwj_emoji_width_two() {
161 let c = Cell::new(
163 "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}",
164 Style::default(),
165 );
166 assert_eq!(c.width, 2);
167 assert!(c.is_wide());
168 }
169
170 #[test]
171 fn cell_from_flag_emoji_width_two() {
172 let c = Cell::new("\u{1F1FA}\u{1F1F8}", Style::default());
174 assert_eq!(c.width, 2);
175 assert!(c.is_wide());
176 }
177
178 #[test]
179 fn cell_from_skin_tone_emoji_width_two() {
180 let c = Cell::new("\u{1F44D}\u{1F3FD}", Style::default());
182 assert_eq!(c.width, 2);
183 assert!(c.is_wide());
184 }
185
186 #[test]
187 fn cell_continuation_after_emoji() {
188 let cont = Cell::continuation();
190 assert!(cont.is_continuation());
191 assert_eq!(cont.width, 0);
192 assert!(cont.grapheme.is_empty());
193 }
194}