ratatui_core/buffer/
cell.rs

1use compact_str::CompactString;
2
3use crate::style::{Color, Modifier, Style};
4use crate::symbols::merge::MergeStrategy;
5
6/// A buffer cell
7#[derive(Debug, Default, Clone)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct Cell {
10    /// The string to be drawn in the cell.
11    ///
12    /// This accepts unicode grapheme clusters which might take up more than one cell.
13    ///
14    /// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
15    /// buffer for short strings.
16    ///
17    /// See <https://github.com/ratatui/ratatui/pull/601> for more information.
18    symbol: Option<CompactString>,
19
20    /// The foreground color of the cell.
21    pub fg: Color,
22
23    /// The background color of the cell.
24    pub bg: Color,
25
26    /// The underline color of the cell.
27    #[cfg(feature = "underline-color")]
28    pub underline_color: Color,
29
30    /// The modifier of the cell.
31    pub modifier: Modifier,
32
33    /// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
34    pub skip: bool,
35}
36
37impl Cell {
38    /// An empty `Cell`
39    pub const EMPTY: Self = Self {
40        symbol: None,
41        fg: Color::Reset,
42        bg: Color::Reset,
43        #[cfg(feature = "underline-color")]
44        underline_color: Color::Reset,
45        modifier: Modifier::empty(),
46        skip: false,
47    };
48
49    /// Creates a new `Cell` with the given symbol.
50    ///
51    /// This works at compile time and puts the symbol onto the stack. Fails to build when the
52    /// symbol doesn't fit onto the stack and requires to be placed on the heap. Use
53    /// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
54    /// details on this.
55    pub const fn new(symbol: &'static str) -> Self {
56        Self {
57            symbol: Some(CompactString::const_new(symbol)),
58            ..Self::EMPTY
59        }
60    }
61
62    /// Gets the symbol of the cell.
63    ///
64    /// If the cell has no symbol, returns a single space character.
65    #[must_use]
66    pub fn symbol(&self) -> &str {
67        self.symbol.as_ref().map_or(" ", |s| s.as_str())
68    }
69
70    /// Merges the symbol of the cell with the one already on the cell, using the provided
71    /// [`MergeStrategy`].
72    ///
73    /// Merges [Box Drawing Unicode block] characters to create a single character representing
74    /// their combination, useful for [border collapsing]. Currently limited to box drawing
75    /// characters, with potential future support for others.
76    ///
77    /// Merging may not be perfect due to Unicode limitations; some symbol combinations might not
78    /// produce a valid character. [`MergeStrategy`] defines how to handle such cases, e.g.,
79    /// `Exact` for valid merges only, or `Fuzzy` for close matches.
80    ///
81    /// If the cell has no symbol set, it will set the symbol to the provided one rather than
82    /// merging.
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// # use ratatui_core::buffer::Cell;
88    /// use ratatui_core::symbols::merge::MergeStrategy;
89    ///
90    /// assert_eq!(
91    ///     Cell::new("┘")
92    ///         .merge_symbol("┏", MergeStrategy::Exact)
93    ///         .symbol(),
94    ///     "╆",
95    /// );
96    ///
97    /// assert_eq!(
98    ///     Cell::new("╭")
99    ///         .merge_symbol("┘", MergeStrategy::Fuzzy)
100    ///         .symbol(),
101    ///     "┼",
102    /// );
103    /// ```
104    ///
105    /// [border collapsing]: https://ratatui.rs/recipes/layout/collapse-borders/
106    /// [Box Drawing Unicode block]: https://en.wikipedia.org/wiki/Box_Drawing
107    pub fn merge_symbol(&mut self, symbol: &str, strategy: MergeStrategy) -> &mut Self {
108        let merged_symbol = self
109            .symbol
110            .as_ref()
111            .map_or(symbol, |s| strategy.merge(s, symbol));
112        self.symbol = Some(CompactString::new(merged_symbol));
113        self
114    }
115
116    /// Sets the symbol of the cell.
117    pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
118        self.symbol = Some(CompactString::new(symbol));
119        self
120    }
121
122    /// Appends a symbol to the cell.
123    ///
124    /// This is particularly useful for adding zero-width characters to the cell.
125    pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
126        self.symbol.get_or_insert_default().push_str(symbol);
127        self
128    }
129
130    /// Sets the symbol of the cell to a single character.
131    pub fn set_char(&mut self, ch: char) -> &mut Self {
132        let mut buf = [0; 4];
133        self.symbol = Some(CompactString::new(ch.encode_utf8(&mut buf)));
134        self
135    }
136
137    /// Sets the foreground color of the cell.
138    pub const fn set_fg(&mut self, color: Color) -> &mut Self {
139        self.fg = color;
140        self
141    }
142
143    /// Sets the background color of the cell.
144    pub const fn set_bg(&mut self, color: Color) -> &mut Self {
145        self.bg = color;
146        self
147    }
148
149    /// Sets the style of the cell.
150    ///
151    ///  `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
152    /// your own type that implements [`Into<Style>`]).
153    pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
154        let style = style.into();
155        if let Some(c) = style.fg {
156            self.fg = c;
157        }
158        if let Some(c) = style.bg {
159            self.bg = c;
160        }
161        #[cfg(feature = "underline-color")]
162        if let Some(c) = style.underline_color {
163            self.underline_color = c;
164        }
165        self.modifier.insert(style.add_modifier);
166        self.modifier.remove(style.sub_modifier);
167        self
168    }
169
170    /// Returns the style of the cell.
171    #[must_use]
172    pub const fn style(&self) -> Style {
173        Style {
174            fg: Some(self.fg),
175            bg: Some(self.bg),
176            #[cfg(feature = "underline-color")]
177            underline_color: Some(self.underline_color),
178            add_modifier: self.modifier,
179            sub_modifier: Modifier::empty(),
180        }
181    }
182
183    /// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
184    ///
185    /// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
186    /// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
187    pub const fn set_skip(&mut self, skip: bool) -> &mut Self {
188        self.skip = skip;
189        self
190    }
191
192    /// Resets the cell to the empty state.
193    pub fn reset(&mut self) {
194        *self = Self::EMPTY;
195    }
196}
197
198impl PartialEq for Cell {
199    /// Compares two `Cell`s for equality.
200    ///
201    /// Note that cells with no symbol (i.e., `Cell::EMPTY`) are considered equal to cells with a
202    /// single space symbol. This is to ensure that empty cells are treated uniformly,
203    /// regardless of how they were created
204    fn eq(&self, other: &Self) -> bool {
205        // Treat None and Some(" ") as equal
206        let symbols_eq = self.symbol() == other.symbol();
207
208        #[cfg(feature = "underline-color")]
209        let underline_color_eq = self.underline_color == other.underline_color;
210        #[cfg(not(feature = "underline-color"))]
211        let underline_color_eq = true;
212
213        symbols_eq
214            && underline_color_eq
215            && self.fg == other.fg
216            && self.bg == other.bg
217            && self.modifier == other.modifier
218            && self.skip == other.skip
219    }
220}
221
222impl Eq for Cell {}
223
224impl core::hash::Hash for Cell {
225    /// Hashes the cell.
226    ///
227    /// This treats symbols with Some(" ") as equal to None, so that empty cells are
228    /// treated uniformly, regardless of how they were created.
229    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
230        self.symbol().hash(state);
231        self.fg.hash(state);
232        self.bg.hash(state);
233        #[cfg(feature = "underline-color")]
234        self.underline_color.hash(state);
235        self.modifier.hash(state);
236        self.skip.hash(state);
237    }
238}
239
240impl From<char> for Cell {
241    fn from(ch: char) -> Self {
242        let mut cell = Self::EMPTY;
243        cell.set_char(ch);
244        cell
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn new() {
254        let cell = Cell::new("あ");
255        assert_eq!(
256            cell,
257            Cell {
258                symbol: Some(CompactString::const_new("あ")),
259                fg: Color::Reset,
260                bg: Color::Reset,
261                #[cfg(feature = "underline-color")]
262                underline_color: Color::Reset,
263                modifier: Modifier::empty(),
264                skip: false,
265            }
266        );
267    }
268
269    #[test]
270    fn empty() {
271        let cell = Cell::EMPTY;
272        assert_eq!(cell.symbol(), " ");
273    }
274
275    #[test]
276    fn set_symbol() {
277        let mut cell = Cell::EMPTY;
278        cell.set_symbol("あ"); // Multi-byte character
279        assert_eq!(cell.symbol(), "あ");
280        cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
281        assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
282    }
283
284    #[test]
285    fn append_symbol() {
286        let mut cell = Cell::EMPTY;
287        cell.set_symbol("あ"); // Multi-byte character
288        cell.append_symbol("\u{200B}"); // zero-width space
289        assert_eq!(cell.symbol(), "あ\u{200B}");
290    }
291
292    #[test]
293    fn set_char() {
294        let mut cell = Cell::EMPTY;
295        cell.set_char('あ'); // Multi-byte character
296        assert_eq!(cell.symbol(), "あ");
297    }
298
299    #[test]
300    fn set_fg() {
301        let mut cell = Cell::EMPTY;
302        cell.set_fg(Color::Red);
303        assert_eq!(cell.fg, Color::Red);
304    }
305
306    #[test]
307    fn set_bg() {
308        let mut cell = Cell::EMPTY;
309        cell.set_bg(Color::Red);
310        assert_eq!(cell.bg, Color::Red);
311    }
312
313    #[test]
314    fn set_style() {
315        let mut cell = Cell::EMPTY;
316        cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
317        assert_eq!(cell.fg, Color::Red);
318        assert_eq!(cell.bg, Color::Blue);
319    }
320
321    #[test]
322    fn set_skip() {
323        let mut cell = Cell::EMPTY;
324        cell.set_skip(true);
325        assert!(cell.skip);
326    }
327
328    #[test]
329    fn reset() {
330        let mut cell = Cell::EMPTY;
331        cell.set_symbol("あ");
332        cell.set_fg(Color::Red);
333        cell.set_bg(Color::Blue);
334        cell.set_skip(true);
335        cell.reset();
336        assert_eq!(cell.symbol(), " ");
337        assert_eq!(cell.fg, Color::Reset);
338        assert_eq!(cell.bg, Color::Reset);
339        assert!(!cell.skip);
340    }
341
342    #[test]
343    fn style() {
344        let cell = Cell::EMPTY;
345        assert_eq!(
346            cell.style(),
347            Style {
348                fg: Some(Color::Reset),
349                bg: Some(Color::Reset),
350                #[cfg(feature = "underline-color")]
351                underline_color: Some(Color::Reset),
352                add_modifier: Modifier::empty(),
353                sub_modifier: Modifier::empty(),
354            }
355        );
356    }
357
358    #[test]
359    fn default() {
360        let cell = Cell::default();
361        assert_eq!(cell.symbol(), " ");
362    }
363
364    #[test]
365    fn cell_eq() {
366        let cell1 = Cell::new("あ");
367        let cell2 = Cell::new("あ");
368        assert_eq!(cell1, cell2);
369    }
370
371    #[test]
372    fn cell_ne() {
373        let cell1 = Cell::new("あ");
374        let cell2 = Cell::new("い");
375        assert_ne!(cell1, cell2);
376    }
377}