Skip to main content

rich_rs/
screen_buffer.rs

1//! ScreenBuffer: a 2D grid of styled cells plus a diff algorithm.
2//!
3//! Rich itself doesn't expose a public "cell buffer" in the same way Textual does, but a
4//! screen buffer + diff is a foundational building block for future TUIs.
5//!
6//! This module provides:
7//! - `Cell` and `ScreenBuffer` (width × height grid)
8//! - Conversion from rendered lines / segments into a `ScreenBuffer`
9//! - A `diff_to_segments` method that produces terminal controls + styled text segments
10//!   to update one buffer into another (cursor-safe, no newlines).
11
12use crate::cells::char_width;
13use crate::segment::{ControlType, Segment, Segments};
14use crate::style::Style;
15use crate::{Console, ConsoleOptions, Renderable};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Cell {
19    /// Text to print at this cell (may be empty for wide continuations).
20    pub text: String,
21    /// Style for this cell.
22    pub style: Option<Style>,
23    /// True if this cell is the trailing continuation of a wide glyph.
24    pub continuation: bool,
25}
26
27impl Cell {
28    pub fn blank(style: Option<Style>) -> Self {
29        Self {
30            text: " ".to_string(),
31            style,
32            continuation: false,
33        }
34    }
35
36    pub fn continuation(style: Option<Style>) -> Self {
37        Self {
38            text: String::new(),
39            style,
40            continuation: true,
41        }
42    }
43
44    pub fn width(&self) -> usize {
45        if self.continuation {
46            0
47        } else {
48            crate::cell_len(&self.text)
49        }
50    }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ScreenBuffer {
55    pub width: usize,
56    pub height: usize,
57    default_style: Option<Style>,
58    cells: Vec<Cell>,
59}
60
61impl ScreenBuffer {
62    pub fn new(width: usize, height: usize, style: Option<Style>) -> Self {
63        let width = width.max(1);
64        let height = height.max(1);
65        Self {
66            width,
67            height,
68            default_style: style,
69            cells: vec![Cell::blank(style); width * height],
70        }
71    }
72
73    fn idx(&self, x: usize, y: usize) -> usize {
74        y * self.width + x
75    }
76
77    pub fn get(&self, x: usize, y: usize) -> &Cell {
78        &self.cells[self.idx(x, y)]
79    }
80
81    pub fn get_mut(&mut self, x: usize, y: usize) -> &mut Cell {
82        let idx = self.idx(x, y);
83        &mut self.cells[idx]
84    }
85
86    pub fn as_plain_lines(&self) -> Vec<String> {
87        let mut lines = Vec::with_capacity(self.height);
88        for y in 0..self.height {
89            let mut line = String::new();
90            for x in 0..self.width {
91                let cell = self.get(x, y);
92                if cell.continuation {
93                    continue;
94                }
95                if cell.text.is_empty() {
96                    line.push(' ');
97                } else {
98                    line.push_str(&cell.text);
99                }
100            }
101            lines.push(crate::cells::set_cell_size(&line, self.width));
102        }
103        lines
104    }
105
106    /// Render a renderable to a ScreenBuffer.
107    ///
108    /// This uses `Console::render_lines` and then converts the rendered lines to cells.
109    pub fn from_renderable(
110        console: &Console,
111        options: &ConsoleOptions,
112        renderable: &dyn Renderable,
113        style: Option<Style>,
114    ) -> Self {
115        let (width, height) = options.size;
116        let lines = console.render_lines(renderable, Some(options), style, true, false);
117        let lines = Segment::set_shape(&lines, width, Some(height), style, false);
118        Self::from_lines(&lines, width, height, style)
119    }
120
121    /// Build a ScreenBuffer from pre-rendered lines.
122    ///
123    /// The caller is expected to provide lines already padded/cropped to `width` × `height`.
124    pub fn from_lines(
125        lines: &[Vec<Segment>],
126        width: usize,
127        height: usize,
128        default_style: Option<Style>,
129    ) -> Self {
130        let mut buffer = ScreenBuffer::new(width, height, default_style);
131
132        for (y, line) in lines.iter().take(height).enumerate() {
133            buffer.write_line(y, line);
134        }
135
136        buffer
137    }
138
139    fn clear_line(&mut self, y: usize) {
140        for x in 0..self.width {
141            *self.get_mut(x, y) = Cell::blank(self.default_style);
142        }
143    }
144
145    fn write_line(&mut self, y: usize, line: &[Segment]) {
146        if y >= self.height {
147            return;
148        }
149        self.clear_line(y);
150
151        let mut x: usize = 0;
152        let mut last_non_zero: Option<(usize, usize)> = None; // (x, width)
153
154        for seg in line {
155            if seg.control.is_some() {
156                continue;
157            }
158            let style = seg.style;
159            for ch in seg.text.chars() {
160                let w = char_width(ch);
161
162                if w == 0 {
163                    // Combine with previous cell, if any.
164                    if let Some((prev_x, prev_w)) = last_non_zero {
165                        let cell = self.get_mut(prev_x, y);
166                        cell.text.push(ch);
167                        // Keep style from the segment currently being processed to match Rich behavior
168                        // for combining marks following styled text.
169                        cell.style = style;
170                        // If previous glyph was wide, combining marks should still attach to the start.
171                        last_non_zero = Some((prev_x, prev_w));
172                    }
173                    continue;
174                }
175
176                if x >= self.width {
177                    return;
178                }
179
180                if w == 2 && x + 1 >= self.width {
181                    // Can't place a wide glyph in the last column; fall back to a space.
182                    *self.get_mut(x, y) = Cell::blank(style);
183                    x += 1;
184                    last_non_zero = Some((x.saturating_sub(1), 1));
185                    continue;
186                }
187
188                *self.get_mut(x, y) = Cell {
189                    text: ch.to_string(),
190                    style,
191                    continuation: false,
192                };
193                last_non_zero = Some((x, w));
194
195                if w == 2 {
196                    *self.get_mut(x + 1, y) = Cell::continuation(style);
197                    x += 2;
198                } else {
199                    x += 1;
200                }
201            }
202        }
203    }
204
205    fn write_line_at(&mut self, y: usize, start_x: usize, max_width: usize, line: &[Segment]) {
206        if y >= self.height {
207            return;
208        }
209        if start_x >= self.width || max_width == 0 {
210            return;
211        }
212
213        let mut x: usize = start_x;
214        let max_x = (start_x + max_width).min(self.width);
215        let mut last_non_zero: Option<(usize, usize)> = None; // (x, width)
216
217        for seg in line {
218            if seg.control.is_some() {
219                continue;
220            }
221            let style = seg.style;
222            for ch in seg.text.chars() {
223                let w = char_width(ch);
224
225                if w == 0 {
226                    if let Some((prev_x, prev_w)) = last_non_zero {
227                        // Only if the previous cell is still inside the region.
228                        if prev_x >= start_x && prev_x < max_x {
229                            let cell = self.get_mut(prev_x, y);
230                            cell.text.push(ch);
231                            cell.style = style;
232                            last_non_zero = Some((prev_x, prev_w));
233                        }
234                    }
235                    continue;
236                }
237
238                if x >= max_x {
239                    return;
240                }
241
242                if w == 2 && x + 1 >= max_x {
243                    // Can't place a wide glyph at the end of the region; fall back to a space.
244                    *self.get_mut(x, y) = Cell::blank(style);
245                    x += 1;
246                    last_non_zero = Some((x.saturating_sub(1), 1));
247                    continue;
248                }
249
250                *self.get_mut(x, y) = Cell {
251                    text: ch.to_string(),
252                    style,
253                    continuation: false,
254                };
255                last_non_zero = Some((x, w));
256
257                if w == 2 {
258                    *self.get_mut(x + 1, y) = Cell::continuation(style);
259                    x += 2;
260                } else {
261                    x += 1;
262                }
263            }
264        }
265    }
266
267    /// Blit pre-rendered lines into the buffer at an offset.
268    ///
269    /// Lines should be padded/cropped to the region width. This method will clip to the
270    /// screen bounds.
271    pub fn blit_lines(&mut self, x: usize, y: usize, width: usize, lines: &[Vec<Segment>]) {
272        if width == 0 {
273            return;
274        }
275        for (row, line) in lines.iter().enumerate() {
276            let yy = y + row;
277            if yy >= self.height {
278                break;
279            }
280            self.write_line_at(yy, x, width, line);
281        }
282    }
283
284    /// Convert the buffer to styled lines (no newlines).
285    pub fn to_styled_lines(&self) -> Vec<Vec<Segment>> {
286        let mut lines: Vec<Vec<Segment>> = Vec::with_capacity(self.height);
287
288        for y in 0..self.height {
289            let mut line: Vec<Segment> = Vec::new();
290            let mut current_style: Option<Style> = None;
291            let mut run = String::new();
292
293            let flush = |line: &mut Vec<Segment>, run: &mut String, style: Option<Style>| {
294                if run.is_empty() {
295                    return;
296                }
297                let mut seg = Segment::new(std::mem::take(run));
298                seg.style = style;
299                line.push(seg);
300            };
301
302            for x in 0..self.width {
303                let cell = self.get(x, y);
304                if cell.continuation {
305                    continue;
306                }
307                let text = if cell.text.is_empty() {
308                    " "
309                } else {
310                    cell.text.as_str()
311                };
312                if cell.style == current_style {
313                    run.push_str(text);
314                } else {
315                    flush(&mut line, &mut run, current_style);
316                    current_style = cell.style;
317                    run.push_str(text);
318                }
319            }
320            flush(&mut line, &mut run, current_style);
321            lines.push(line);
322        }
323
324        lines
325    }
326
327    fn cell_span_width(&self, x: usize, y: usize) -> usize {
328        let cell = self.get(x, y);
329        if cell.continuation {
330            0
331        } else {
332            let w = cell.width();
333            if w == 0 { 1 } else { w }
334        }
335    }
336
337    /// Compute an update sequence that transforms `previous` into `self`.
338    ///
339    /// The returned segments:
340    /// - Optionally start with `Home` (cursor to 0,0)
341    /// - Use cursor controls (no `\n`) for positioning
342    /// - Emit styled text for changed spans
343    fn diff_to_segments_impl(&self, previous: &ScreenBuffer, include_home: bool) -> Segments {
344        assert_eq!(self.width, previous.width, "buffer widths differ");
345        assert_eq!(self.height, previous.height, "buffer heights differ");
346
347        let mut out = Segments::new();
348        if include_home {
349            out.push(Segment::control(ControlType::Home));
350        }
351
352        let mut cursor_x: usize = 0;
353        let mut cursor_y: usize = 0;
354
355        for y in 0..self.height {
356            let mut x: usize = 0;
357
358            while x < self.width {
359                let curr = self.get(x, y);
360                let prev = previous.get(x, y);
361
362                // Never start updates on continuation cells.
363                if curr.continuation || prev.continuation {
364                    x += 1;
365                    continue;
366                }
367
368                if curr == prev {
369                    x += 1;
370                    continue;
371                }
372
373                let mut span = self
374                    .cell_span_width(x, y)
375                    .max(previous.cell_span_width(x, y))
376                    .max(1);
377                span = span.min(self.width.saturating_sub(x));
378
379                // Extend span over subsequent differing cells.
380                let mut end_x = x + span;
381                while end_x < self.width {
382                    let c = self.get(end_x, y);
383                    let p = previous.get(end_x, y);
384                    if c.continuation || p.continuation {
385                        end_x += 1;
386                        continue;
387                    }
388                    if c == p {
389                        break;
390                    }
391                    let extra = self
392                        .cell_span_width(end_x, y)
393                        .max(previous.cell_span_width(end_x, y))
394                        .max(1);
395                    end_x = (end_x + extra).min(self.width);
396                }
397
398                // Move cursor to (x, y)
399                if y != cursor_y {
400                    if y > cursor_y {
401                        out.push(Segment::control(ControlType::CursorDown(
402                            (y - cursor_y) as u16,
403                        )));
404                    } else {
405                        out.push(Segment::control(ControlType::CursorUp(
406                            (cursor_y - y) as u16,
407                        )));
408                    }
409                    cursor_y = y;
410                    cursor_x = 0;
411                    out.push(Segment::control(ControlType::CarriageReturn));
412                }
413
414                if x != cursor_x {
415                    // Normalize to start-of-line then move forward.
416                    out.push(Segment::control(ControlType::CarriageReturn));
417                    cursor_x = 0;
418                    if x > 0 {
419                        out.push(Segment::control(ControlType::CursorForward(x as u16)));
420                        cursor_x = x;
421                    }
422                }
423
424                // Emit the updated span as styled segments.
425                let mut run_x = x;
426                while run_x < end_x {
427                    let cell = self.get(run_x, y);
428                    if cell.continuation {
429                        run_x += 1;
430                        continue;
431                    }
432                    let w = self.cell_span_width(run_x, y).max(1);
433                    let text = if cell.text.is_empty() {
434                        " ".to_string()
435                    } else {
436                        cell.text.clone()
437                    };
438                    let mut seg = Segment::new(text);
439                    seg.style = cell.style;
440                    out.push(seg);
441                    cursor_x += w;
442                    run_x += w;
443                }
444
445                x = end_x;
446            }
447        }
448
449        // Leave cursor at column 0 on the last row so live render cursor math remains stable.
450        let target_y = self.height.saturating_sub(1);
451        if target_y != cursor_y {
452            if target_y > cursor_y {
453                out.push(Segment::control(ControlType::CursorDown(
454                    (target_y - cursor_y) as u16,
455                )));
456            } else {
457                out.push(Segment::control(ControlType::CursorUp(
458                    (cursor_y - target_y) as u16,
459                )));
460            }
461        }
462        out.push(Segment::control(ControlType::CarriageReturn));
463
464        out
465    }
466
467    /// Compute an update sequence that transforms `previous` into `self`.
468    ///
469    /// The returned segments:
470    /// - Start with `Home` (cursor to 0,0)
471    /// - Use cursor controls (no `\n`) for positioning
472    /// - Emit styled text for changed spans
473    pub fn diff_to_segments(&self, previous: &ScreenBuffer) -> Segments {
474        self.diff_to_segments_impl(previous, true)
475    }
476
477    /// Compute an update sequence relative to the current cursor origin.
478    ///
479    /// Unlike `diff_to_segments`, this does *not* emit `Home` and is intended for
480    /// embedding in larger cursor-positioned render flows (e.g. Live updates).
481    pub fn diff_to_segments_from_origin(&self, previous: &ScreenBuffer) -> Segments {
482        self.diff_to_segments_impl(previous, false)
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::Text;
490
491    fn apply_segments(mut buffer: ScreenBuffer, segments: &Segments) -> ScreenBuffer {
492        let mut x: usize = 0;
493        let mut y: usize = 0;
494        let mut last_non_zero: Option<(usize, usize)> = None; // (x, width)
495
496        let width = buffer.width;
497        let height = buffer.height;
498
499        for seg in segments.iter() {
500            if let Some(ctrl) = &seg.control {
501                match ctrl {
502                    ControlType::Home => {
503                        x = 0;
504                        y = 0;
505                    }
506                    ControlType::CarriageReturn => x = 0,
507                    ControlType::CursorUp(n) => y = y.saturating_sub(*n as usize),
508                    ControlType::CursorDown(n) => {
509                        y = (y + *n as usize).min(height.saturating_sub(1))
510                    }
511                    ControlType::CursorForward(n) => {
512                        x = (x + *n as usize).min(width.saturating_sub(1))
513                    }
514                    ControlType::CursorBackward(n) => x = x.saturating_sub(*n as usize),
515                    _ => {}
516                }
517                continue;
518            }
519
520            for ch in seg.text.chars() {
521                let w = char_width(ch);
522                if x >= width || y >= height {
523                    break;
524                }
525
526                if w == 0 {
527                    if let Some((prev_x, prev_w)) = last_non_zero {
528                        let cell = buffer.get_mut(prev_x, y);
529                        cell.text.push(ch);
530                        cell.style = seg.style;
531                        last_non_zero = Some((prev_x, prev_w));
532                    }
533                    continue;
534                }
535
536                if w == 2 && x + 1 >= width {
537                    break;
538                }
539                *buffer.get_mut(x, y) = Cell {
540                    text: ch.to_string(),
541                    style: seg.style,
542                    continuation: false,
543                };
544                last_non_zero = Some((x, w));
545                if w == 2 {
546                    *buffer.get_mut(x + 1, y) = Cell::continuation(seg.style);
547                    x += 2;
548                } else {
549                    x += 1;
550                }
551            }
552        }
553
554        buffer
555    }
556
557    #[test]
558    fn test_screen_buffer_from_renderable_plain() {
559        let console = Console::new();
560        let mut options = console.options().clone();
561        options.size = (5, 2);
562        options.max_width = 5;
563        options.max_height = 2;
564
565        let buf = ScreenBuffer::from_renderable(&console, &options, &Text::plain("hi"), None);
566        assert_eq!(buf.as_plain_lines()[0], "hi   ");
567        assert_eq!(buf.as_plain_lines()[1], "     ");
568    }
569
570    #[test]
571    fn test_screen_buffer_diff_applies() {
572        let console = Console::new();
573        let mut options = console.options().clone();
574        options.size = (10, 3);
575        options.max_width = 10;
576        options.max_height = 3;
577
578        let prev = ScreenBuffer::from_renderable(&console, &options, &Text::plain("A"), None);
579        let next = ScreenBuffer::from_renderable(&console, &options, &Text::plain("B"), None);
580
581        let diff = next.diff_to_segments(&prev);
582        let applied = apply_segments(prev.clone(), &diff);
583        assert_eq!(applied, next);
584    }
585
586    #[test]
587    fn test_screen_buffer_diff_handles_wide_char() {
588        let console = Console::new();
589        let mut options = console.options().clone();
590        options.size = (6, 1);
591        options.max_width = 6;
592        options.max_height = 1;
593
594        // Wide CJK character (2 cells)
595        let prev = ScreenBuffer::from_renderable(&console, &options, &Text::plain("你"), None);
596        let next = ScreenBuffer::from_renderable(&console, &options, &Text::plain("a"), None);
597
598        let diff = next.diff_to_segments(&prev);
599        let applied = apply_segments(prev.clone(), &diff);
600        assert_eq!(applied, next);
601    }
602
603    #[test]
604    fn test_screen_buffer_diff_uses_no_newlines() {
605        let console = Console::new();
606        let mut options = console.options().clone();
607        options.size = (10, 2);
608        options.max_width = 10;
609        options.max_height = 2;
610
611        let prev = ScreenBuffer::from_renderable(&console, &options, &Text::plain("A"), None);
612        let next = ScreenBuffer::from_renderable(&console, &options, &Text::plain("B"), None);
613        let diff = next.diff_to_segments(&prev);
614
615        assert!(diff.iter().all(|s| !s.text.contains('\n')));
616    }
617}