Skip to main content

ratatui_core/buffer/
cell.rs

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