dreg_core/
buffer.rs

1//! Buffer
2
3
4
5use compact_str::CompactString;
6use unicode_segmentation::UnicodeSegmentation;
7use unicode_width::UnicodeWidthStr as _;
8
9use crate::prelude::*;
10
11
12
13#[derive(Clone, Default, Eq, Hash, PartialEq)]
14pub struct Buffer {
15    /// The area represented by this buffer
16    pub area: Rect,
17    /// The content of the buffer. The length of this Vec should always be equal to area.width *
18    /// area.height
19    pub content: Vec<Cell>,
20}
21
22impl Buffer {
23    /// Returns a Buffer with all cells set to the default one
24    #[must_use]
25    pub fn empty(area: Rect) -> Self {
26        Self::filled(area, Cell::EMPTY)
27    }
28
29    /// Returns a Buffer with all cells initialized with the attributes of the given Cell
30    #[must_use]
31    pub fn filled(area: Rect, cell: Cell) -> Self {
32        let size = area.area() as usize;
33        let content = vec![cell; size];
34        Self { area, content }
35    }
36
37    /// Returns the content of the buffer as a slice
38    pub fn content(&self) -> &[Cell] {
39        &self.content
40    }
41
42    /// Returns the area covered by this buffer
43    pub const fn area(&self) -> &Rect {
44        &self.area
45    }
46
47    /// Returns a reference to Cell at the given coordinates
48    #[track_caller]
49    pub fn get(&self, x: u16, y: u16) -> &Cell {
50        let i = self.index_of(x, y);
51        &self.content[i]
52    }
53
54    /// Returns a mutable reference to Cell at the given coordinates
55    #[track_caller]
56    pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
57        let i = self.index_of(x, y);
58        &mut self.content[i]
59    }
60
61    /// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
62    ///
63    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
64    ///
65    /// # Panics
66    ///
67    /// Panics when given an coordinate that is outside of this Buffer's area.
68    #[track_caller]
69    pub fn index_of(&self, x: u16, y: u16) -> usize {
70        debug_assert!(
71            x >= self.area.left()
72                && x < self.area.right()
73                && y >= self.area.top()
74                && y < self.area.bottom(),
75            "Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
76            self.area
77        );
78        ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
79    }
80
81    /// Returns the (global) coordinates of a cell given its index
82    ///
83    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
84    ///
85    /// # Panics
86    ///
87    /// Panics when given an index that is outside the Buffer's content.
88    pub fn pos_of(&self, i: usize) -> (u16, u16) {
89        debug_assert!(
90            i < self.content.len(),
91            "Trying to get the coords of a cell outside the buffer: i={i} len={}",
92            self.content.len()
93        );
94        (
95            self.area.x + (i as u16) % self.area.width,
96            self.area.y + (i as u16) / self.area.width,
97        )
98    }
99
100    /// Print a string, starting at the position (x, y)
101    pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
102    where
103        T: AsRef<str>,
104        S: Into<Style>,
105    {
106        self.set_stringn(x, y, string, usize::MAX, style);
107    }
108
109    /// Print at most the first n characters of a string if enough space is available
110    /// until the end of the line.
111    ///
112    /// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
113    pub fn set_stringn<T, S>(
114        &mut self,
115        mut x: u16,
116        y: u16,
117        string: T,
118        max_width: usize,
119        style: S,
120    ) -> (u16, u16)
121    where
122        T: AsRef<str>,
123        S: Into<Style>,
124    {
125        let max_width = max_width.try_into().unwrap_or(u16::MAX);
126        let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
127        let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
128            .map(|symbol| (symbol, symbol.width() as u16))
129            .filter(|(_symbol, width)| *width > 0)
130            .map_while(|(symbol, width)| {
131                remaining_width = remaining_width.checked_sub(width)?;
132                Some((symbol, width))
133            });
134        let style = style.into();
135        for (symbol, width) in graphemes {
136            self.get_mut(x, y).set_symbol(symbol).set_style(style);
137            let next_symbol = x + width;
138            x += 1;
139            // Reset following cells if multi-width (they would be hidden by the grapheme),
140            while x < next_symbol {
141                self.get_mut(x, y).reset();
142                x += 1;
143            }
144        }
145        (x, y)
146    }
147
148    // /// Print a label, starting at the position (x, y)
149    // pub fn set_label(&mut self, x: u16, y: u16, label: &Label<'_>, max_width: u16) -> (u16, u16) {
150    //     self.set_stringn(x, y, &label.content, max_width as usize, label.style)
151    // }
152
153    /// Set the style of all cells in the given area.
154    ///
155    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
156    /// your own type that implements [`Into<Style>`]).
157    pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
158        let style = style.into();
159        let area = self.area.intersection(area);
160        for y in area.top()..area.bottom() {
161            for x in area.left()..area.right() {
162                self.get_mut(x, y).set_style(style);
163            }
164        }
165    }
166
167    /// Resize the buffer so that the mapped area matches the given area and that the buffer
168    /// length is equal to area.width * area.height
169    pub fn resize(&mut self, area: Rect) {
170        let length = area.area() as usize;
171        if self.content.len() > length {
172            self.content.truncate(length);
173        } else {
174            self.content.resize(length, Cell::EMPTY);
175        }
176        self.area = area;
177    }
178
179    /// Reset all cells in the buffer
180    pub fn reset(&mut self) {
181        for cell in &mut self.content {
182            cell.reset();
183        }
184    }
185
186    /// Merge an other buffer into this one
187    pub fn merge(&mut self, other: &Self) {
188        let area = self.area.union(other.area);
189        self.content.resize(area.area() as usize, Cell::EMPTY);
190
191        // Move original content to the appropriate space
192        let size = self.area.area() as usize;
193        for i in (0..size).rev() {
194            let (x, y) = self.pos_of(i);
195            // New index in content
196            let k = ((y - area.y) * area.width + x - area.x) as usize;
197            if i != k {
198                self.content[k] = self.content[i].clone();
199                self.content[i].reset();
200            }
201        }
202
203        // Push content of the other buffer into this one (may erase previous
204        // data)
205        let size = other.area.area() as usize;
206        for i in 0..size {
207            let (x, y) = other.pos_of(i);
208            // New index in content
209            let k = ((y - area.y) * area.width + x - area.x) as usize;
210            self.content[k] = other.content[i].clone();
211        }
212        self.area = area;
213    }
214
215    /// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
216    /// self to other.
217    ///
218    /// We're assuming that buffers are well-formed, that is no double-width cell is followed by
219    /// a non-blank cell.
220    ///
221    /// # Multi-width characters handling:
222    ///
223    /// ```text
224    /// (Index:) `01`
225    /// Prev:    `コ`
226    /// Next:    `aa`
227    /// Updates: `0: a, 1: a'
228    /// ```
229    ///
230    /// ```text
231    /// (Index:) `01`
232    /// Prev:    `a `
233    /// Next:    `コ`
234    /// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
235    /// ```
236    ///
237    /// ```text
238    /// (Index:) `012`
239    /// Prev:    `aaa`
240    /// Next:    `aコ`
241    /// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
242    /// ```
243    pub fn diff<'a>(&self, other: &'a Self) -> Vec<(u16, u16, &'a Cell)> {
244        let prev_buf = &self.content;
245        let next_buf = &other.content;
246
247        let mut updates: Vec<(u16, u16, &Cell)> = vec![];
248        // Cells invalidated by drawing/replacing preceding multi-width characters:
249        let mut invalidated: usize = 0;
250        // Cells from the current buffer to skip due to preceding multi-width characters taking
251        // their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
252        let mut to_skip: usize = 0;
253        for (i, (current, previous)) in next_buf.iter().zip(prev_buf.iter()).enumerate() {
254            if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
255                let (x, y) = self.pos_of(i);
256                updates.push((x, y, &next_buf[i]));
257            }
258
259            to_skip = current.symbol().width().saturating_sub(1);
260
261            let affected_width = std::cmp::max(
262                current.symbol().width(),
263                previous.symbol().width(),
264            );
265            invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
266        }
267        updates
268    }
269}
270
271impl std::fmt::Debug for Buffer {
272    /// Writes a debug representation of the buffer to the given formatter.
273    ///
274    /// The format is like a pretty printed struct, with the following fields:
275    /// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
276    /// * `content`: displayed as a list of strings representing the content of the buffer
277    /// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
278    ///   modifier: Modifier::BOLD }` only showing a value when there is a change in style.
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        f.write_fmt(format_args!("Buffer {{\n    area: {:?}", &self.area))?;
281
282        if self.area.is_empty() {
283            return f.write_str("\n}");
284        }
285
286        f.write_str(",\n    content: [\n")?;
287        let mut last_style = None;
288        let mut styles = vec![];
289        for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
290            let mut overwritten = vec![];
291            let mut skip: usize = 0;
292            f.write_str("        \"")?;
293            for (x, c) in line.iter().enumerate() {
294                if skip == 0 {
295                    f.write_str(c.symbol())?;
296                } else {
297                    overwritten.push((x, c.symbol()));
298                }
299                skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
300                #[cfg(feature = "underline-color")]
301                {
302                    let style = (c.fg, c.bg, c.underline_color, c.modifier);
303                    if last_style != Some(style) {
304                        last_style = Some(style);
305                        styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
306                    }
307                }
308                #[cfg(not(feature = "underline-color"))]
309                {
310                    let style = (c.fg, c.bg, c.modifier);
311                    if last_style != Some(style) {
312                        last_style = Some(style);
313                        styles.push((x, y, c.fg, c.bg, c.modifier));
314                    }
315                }
316            }
317            f.write_str("\",")?;
318            if !overwritten.is_empty() {
319                f.write_fmt(format_args!(
320                    " // hidden by multi-width symbols: {overwritten:?}"
321                ))?;
322            }
323            f.write_str("\n")?;
324        }
325        f.write_str("    ],\n    styles: [\n")?;
326        for s in styles {
327            #[cfg(feature = "underline-color")]
328            f.write_fmt(format_args!(
329                "        x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
330                s.0, s.1, s.2, s.3, s.4, s.5
331            ))?;
332            #[cfg(not(feature = "underline-color"))]
333            f.write_fmt(format_args!(
334                "        x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
335                s.0, s.1, s.2, s.3, s.4
336            ))?;
337        }
338        f.write_str("    ]\n}")?;
339        Ok(())
340    }
341}
342
343
344
345/// A buffer cell
346#[derive(Clone, Debug, Eq, Hash, PartialEq)]
347pub struct Cell {
348    /// The string to be drawn in the cell.
349    ///
350    /// This accepts unicode grapheme clusters which might take up more than one cell.
351    ///
352    /// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
353    /// buffer for short strings.
354    ///
355    /// See <https://github.com/ratatui-org/ratatui/pull/601> for more information.
356    symbol: CompactString,
357
358    /// The foreground color of the cell.
359    pub fg: Color,
360
361    /// The background color of the cell.
362    pub bg: Color,
363
364    /// The underline color of the cell.
365    #[cfg(feature = "underline-color")]
366    pub underline_color: Color,
367
368    /// The modifier of the cell.
369    pub modifier: Modifier,
370
371    /// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
372    pub skip: bool,
373}
374
375impl Cell {
376    /// An empty `Cell`
377    pub const EMPTY: Self = Self::new(" ");
378
379    /// Creates a new `Cell` with the given symbol.
380    ///
381    /// This works at compile time and puts the symbol onto the stack. Fails to build when the
382    /// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
383    /// `Self::default().set_symbol()` in that case. See [`CompactString::new_inline`] for more
384    /// details on this.
385    pub const fn new(symbol: &'static str) -> Self {
386        Self {
387            symbol: CompactString::new_inline(symbol),
388            fg: Color::Reset,
389            bg: Color::Reset,
390            #[cfg(feature = "underline-color")]
391            underline_color: Color::Reset,
392            modifier: Modifier::empty(),
393            skip: false,
394        }
395    }
396
397    /// Gets the symbol of the cell.
398    #[must_use]
399    pub fn symbol(&self) -> &str {
400        self.symbol.as_str()
401    }
402
403    /// Sets the symbol of the cell.
404    pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
405        self.symbol = CompactString::new(symbol);
406        self
407    }
408
409    /// Appends a symbol to the cell.
410    ///
411    /// This is particularly useful for adding zero-width characters to the cell.
412    pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
413        self.symbol.push_str(symbol);
414        self
415    }
416
417    /// Sets the symbol of the cell to a single character.
418    pub fn set_char(&mut self, ch: char) -> &mut Self {
419        let mut buf = [0; 4];
420        self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
421        self
422    }
423
424    /// Sets the foreground color of the cell.
425    pub fn set_fg(&mut self, color: Color) -> &mut Self {
426        self.fg = color;
427        self
428    }
429
430    /// Sets the background color of the cell.
431    pub fn set_bg(&mut self, color: Color) -> &mut Self {
432        self.bg = color;
433        self
434    }
435
436    /// Sets the style of the cell.
437    ///
438    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
439    /// your own type that implements [`Into<Style>`]).
440    pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
441        let style = style.into();
442        if let Some(c) = style.fg {
443            self.fg = c;
444        }
445        if let Some(c) = style.bg {
446            self.bg = c;
447        }
448        #[cfg(feature = "underline-color")]
449        if let Some(c) = style.underline_color {
450            self.underline_color = c;
451        }
452        self.modifier.insert(style.add_modifier);
453        self.modifier.remove(style.sub_modifier);
454        self
455    }
456
457    /// Returns the style of the cell.
458    #[must_use]
459    pub const fn style(&self) -> Style {
460        Style {
461            color_mode: ColorMode::overwrite(),
462            fg: Some(self.fg),
463            bg: Some(self.bg),
464            #[cfg(feature = "underline-color")]
465            underline_color: Some(self.underline_color),
466            add_modifier: self.modifier,
467            sub_modifier: Modifier::empty(),
468        }
469    }
470
471    /// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
472    ///
473    /// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
474    /// covered by an image.
475    pub fn set_skip(&mut self, skip: bool) -> &mut Self {
476        self.skip = skip;
477        self
478    }
479
480    /// Resets the cell to the empty state.
481    pub fn reset(&mut self) {
482        self.symbol = CompactString::new_inline(" ");
483        self.fg = Color::Reset;
484        self.bg = Color::Reset;
485        #[cfg(feature = "underline-color")]
486        {
487            self.underline_color = Color::Reset;
488        }
489        self.modifier = Modifier::empty();
490        self.skip = false;
491    }
492}
493
494impl Default for Cell {
495    fn default() -> Self {
496        Self::EMPTY
497    }
498}