Skip to main content

rich_ext/surface/
screen.rs

1//! The cell screen and its painter, which [`Surface`](super::Surface) draws
2//! with. Moved here from `rs-rich-intuituive`, which re-exports them.
3//!
4//! A [`Screen`] is a grid of [`Cell`]s, each a grapheme, its width and an
5//! interned rich [`Style`] (so hyperlinks and every rich attribute survive,
6//! which a ratatui cell cannot hold). Callers write rendered lines into a
7//! rectangle; the [`Painter`] compares only the damaged rectangles with
8//! what it sent last time and writes the difference, keeping colour and
9//! link state across the frame rather than resetting it for every run.
10
11use std::collections::HashMap;
12
13use rich::{ColorSystem, Segment, Style};
14
15use crate::frame::{Frame, FxBuild, Rgb, StyleId, StyleRemap, StyleTable};
16
17/// A rectangle of cells.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub struct Rect {
20    pub x: u16,
21    pub y: u16,
22    pub width: u16,
23    pub height: u16,
24}
25
26impl Rect {
27    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
28        Rect {
29            x,
30            y,
31            width,
32            height,
33        }
34    }
35
36    pub fn right(&self) -> u16 {
37        self.x.saturating_add(self.width)
38    }
39
40    pub fn bottom(&self) -> u16 {
41        self.y.saturating_add(self.height)
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.width == 0 || self.height == 0
46    }
47
48    pub fn contains(&self, column: u16, row: u16) -> bool {
49        column >= self.x && column < self.right() && row >= self.y && row < self.bottom()
50    }
51
52    /// The part of this rectangle inside `other`.
53    #[must_use]
54    pub fn intersection(&self, other: Rect) -> Rect {
55        let x = self.x.max(other.x);
56        let y = self.y.max(other.y);
57        let right = self.right().min(other.right());
58        let bottom = self.bottom().min(other.bottom());
59        Rect::new(x, y, right.saturating_sub(x), bottom.saturating_sub(y))
60    }
61
62    /// This rectangle without a border of `n` cells on each side.
63    #[must_use]
64    pub fn inner(&self, n: u16) -> Rect {
65        Rect::new(
66            self.x.saturating_add(n),
67            self.y.saturating_add(n),
68            self.width.saturating_sub(n * 2),
69            self.height.saturating_sub(n * 2),
70        )
71    }
72
73    /// This rectangle without `top`, `right`, `bottom` and `left` cells on
74    /// those sides (CSS's order), never less than empty.
75    ///
76    /// ```
77    /// use rich_ext::surface::Rect;
78    ///
79    /// assert_eq!(Rect::new(0, 0, 10, 5).inset(1, 2, 0, 3), Rect::new(3, 1, 5, 4));
80    /// assert!(Rect::new(0, 0, 4, 4).inset(0, 3, 0, 3).is_empty());
81    /// ```
82    #[must_use]
83    pub fn inset(&self, top: u16, right: u16, bottom: u16, left: u16) -> Rect {
84        let width = self.width.saturating_sub(left.saturating_add(right));
85        let height = self.height.saturating_sub(top.saturating_add(bottom));
86        Rect::new(
87            self.x.saturating_add(left.min(self.width)),
88            self.y.saturating_add(top.min(self.height)),
89            width,
90            height,
91        )
92    }
93
94    /// A rectangle `width` x `height` in the middle of this one, cut to it
95    /// when it is larger (an odd cell left over goes after it): where a
96    /// pop-up or a dialog goes.
97    ///
98    /// ```
99    /// use rich_ext::surface::Rect;
100    ///
101    /// assert_eq!(Rect::new(0, 0, 20, 10).centered(8, 3), Rect::new(6, 3, 8, 3));
102    /// assert_eq!(Rect::new(2, 2, 4, 4).centered(10, 2), Rect::new(2, 3, 4, 2));
103    /// ```
104    #[must_use]
105    pub fn centered(&self, width: u16, height: u16) -> Rect {
106        let (width, height) = (width.min(self.width), height.min(self.height));
107        Rect::new(
108            self.x + (self.width - width) / 2,
109            self.y + (self.height - height) / 2,
110            width,
111            height,
112        )
113    }
114
115    /// This rectangle moved `dx` columns right and `dy` rows down (left and
116    /// up when negative), its size kept and its corner kept on the grid:
117    /// at least 0, and no further than its size allows.
118    ///
119    /// ```
120    /// use rich_ext::surface::Rect;
121    ///
122    /// assert_eq!(Rect::new(5, 5, 2, 2).offset(3, -2), Rect::new(8, 3, 2, 2));
123    /// assert_eq!(Rect::new(1, 1, 2, 2).offset(-4, 0), Rect::new(0, 1, 2, 2));
124    /// ```
125    #[must_use]
126    pub fn offset(&self, dx: i32, dy: i32) -> Rect {
127        let moved = |at: u16, by: i32, size: u16| {
128            (i32::from(at) + by).clamp(0, i32::from(u16::MAX - size)) as u16
129        };
130        Rect::new(
131            moved(self.x, dx, self.width),
132            moved(self.y, dy, self.height),
133            self.width,
134            self.height,
135        )
136    }
137
138    /// The smallest rectangle holding both. An empty rectangle holds
139    /// nothing, so it adds nothing.
140    ///
141    /// ```
142    /// use rich_ext::surface::Rect;
143    ///
144    /// let a = Rect::new(0, 0, 2, 2);
145    /// assert_eq!(a.union(Rect::new(5, 1, 1, 4)), Rect::new(0, 0, 6, 5));
146    /// assert_eq!(a.union(Rect::default()), a);
147    /// ```
148    #[must_use]
149    pub fn union(&self, other: Rect) -> Rect {
150        if other.is_empty() {
151            return *self;
152        }
153        if self.is_empty() {
154            return other;
155        }
156        let (x, y) = (self.x.min(other.x), self.y.min(other.y));
157        let right = self.right().max(other.right());
158        let bottom = self.bottom().max(other.bottom());
159        Rect::new(x, y, right - x, bottom - y)
160    }
161
162    /// Whether the two share a cell.
163    ///
164    /// ```
165    /// use rich_ext::surface::Rect;
166    ///
167    /// let a = Rect::new(0, 0, 4, 4);
168    /// assert!(a.intersects(Rect::new(3, 3, 2, 2)));
169    /// assert!(!a.intersects(Rect::new(4, 0, 2, 2)), "touching is not sharing");
170    /// ```
171    pub fn intersects(&self, other: Rect) -> bool {
172        !self.intersection(other).is_empty()
173    }
174
175    /// Each of its rows, top to bottom, as a rectangle one cell tall.
176    ///
177    /// ```
178    /// use rich_ext::surface::Rect;
179    ///
180    /// let rows: Vec<Rect> = Rect::new(1, 2, 3, 2).rows().collect();
181    /// assert_eq!(rows, [Rect::new(1, 2, 3, 1), Rect::new(1, 3, 3, 1)]);
182    /// ```
183    pub fn rows(self) -> impl DoubleEndedIterator<Item = Rect> + ExactSizeIterator {
184        (self.y..self.bottom()).map(move |y| Rect::new(self.x, y, self.width, 1))
185    }
186
187    /// Each of its columns, left to right, as a rectangle one cell wide.
188    ///
189    /// ```
190    /// use rich_ext::surface::Rect;
191    ///
192    /// let columns: Vec<Rect> = Rect::new(1, 2, 2, 3).columns().collect();
193    /// assert_eq!(columns, [Rect::new(1, 2, 1, 3), Rect::new(2, 2, 1, 3)]);
194    /// ```
195    pub fn columns(self) -> impl DoubleEndedIterator<Item = Rect> + ExactSizeIterator {
196        (self.x..self.right()).map(move |x| Rect::new(x, self.y, 1, self.height))
197    }
198}
199
200/// One cell: a grapheme and its style. A wide grapheme's second column is a
201/// continuation cell (empty text, width 0).
202///
203/// A cell is small and `Copy`, with no heap string: a grapheme of up to
204/// four bytes (every single character, and short clusters such as `e` and
205/// a combining accent) is kept inline, and a longer one (an emoji joined
206/// with ZWJs) as an index into a table of every such grapheme seen, which
207/// is shared by all screens and only grows. Comparing, copying and
208/// scrolling cells moves plain data, as a game keeps its hot data flat.
209///
210/// ```
211/// use rich_ext::frame::StyleId;
212/// use rich_ext::surface::Cell;
213///
214/// let a = Cell::new("é", 1, StyleId::NONE);
215/// let family = Cell::new("👨\u{200d}👩\u{200d}👧", 2, StyleId::NONE);
216/// assert_eq!((a.text(), family.text()), ("é", "👨\u{200d}👩\u{200d}👧"));
217/// assert_eq!(family, Cell::new("👨\u{200d}👩\u{200d}👧", 2, StyleId::NONE));
218/// assert_eq!(std::mem::size_of::<Cell>(), 12);
219/// ```
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
221pub struct Cell {
222    /// The grapheme's UTF-8, or a long grapheme's index (little-endian).
223    bytes: [u8; 4],
224    /// How many of `bytes` are the text, or [`LONG`] for an index.
225    len: u8,
226    pub width: u8,
227    pub style: StyleId,
228}
229
230/// [`Cell::len`] for a grapheme kept in the shared table.
231const LONG: u8 = u8::MAX;
232
233/// The graphemes longer than four bytes, each once, for every screen: what
234/// a cell's index names. Leaked, so a cell's text is borrowed for as long
235/// as the program runs.
236struct Graphemes {
237    texts: Vec<&'static str>,
238    index: HashMap<&'static str, u32>,
239}
240
241fn graphemes() -> &'static std::sync::RwLock<Graphemes> {
242    static TABLE: std::sync::OnceLock<std::sync::RwLock<Graphemes>> = std::sync::OnceLock::new();
243    TABLE.get_or_init(|| {
244        std::sync::RwLock::new(Graphemes {
245            texts: Vec::new(),
246            index: HashMap::new(),
247        })
248    })
249}
250
251impl Cell {
252    /// A cell showing `text` (one grapheme), `width` columns wide (0 for a
253    /// continuation), in `style`.
254    pub fn new(text: &str, width: u8, style: StyleId) -> Cell {
255        let mut bytes = [0u8; 4];
256        let len = if text.len() <= 4 {
257            bytes[..text.len()].copy_from_slice(text.as_bytes());
258            text.len() as u8
259        } else {
260            let known = graphemes()
261                .read()
262                .unwrap_or_else(|e| e.into_inner())
263                .index
264                .get(text)
265                .copied();
266            let at = known.unwrap_or_else(|| {
267                let mut table = graphemes().write().unwrap_or_else(|e| e.into_inner());
268                if let Some(&at) = table.index.get(text) {
269                    return at;
270                }
271                let at = table.texts.len() as u32;
272                let text: &'static str = Box::leak(text.to_string().into_boxed_str());
273                table.texts.push(text);
274                table.index.insert(text, at);
275                at
276            });
277            bytes = at.to_le_bytes();
278            LONG
279        };
280        Cell {
281            bytes,
282            len,
283            width,
284            style,
285        }
286    }
287
288    /// A single character, `width` columns wide.
289    #[inline]
290    fn char(c: char, width: u8, style: StyleId) -> Cell {
291        let mut bytes = [0u8; 4];
292        let len = c.encode_utf8(&mut bytes).len() as u8;
293        Cell {
294            bytes,
295            len,
296            width,
297            style,
298        }
299    }
300
301    fn blank() -> Cell {
302        Cell::char(' ', 1, StyleId::NONE)
303    }
304
305    /// A single character, `width` columns wide: [`Cell::new`] of it.
306    #[inline]
307    pub(crate) fn from_char(c: char, width: u8, style: StyleId) -> Cell {
308        Cell::char(c, width, style)
309    }
310
311    /// The cell's text when it is one character, decoded from its bytes
312    /// without a UTF-8 check (they were written from a `char` or a `&str`).
313    #[inline]
314    pub(crate) fn single_char(&self) -> Option<char> {
315        let [b0, b1, b2, b3] = self.bytes.map(u32::from);
316        let code = match self.len {
317            1 => b0,
318            2 if b0 & 0xe0 == 0xc0 => (b0 & 0x1f) << 6 | (b1 & 0x3f),
319            3 if b0 & 0xf0 == 0xe0 => (b0 & 0x0f) << 12 | (b1 & 0x3f) << 6 | (b2 & 0x3f),
320            4 if b0 & 0xf8 == 0xf0 => {
321                (b0 & 0x07) << 18 | (b1 & 0x3f) << 12 | (b2 & 0x3f) << 6 | (b3 & 0x3f)
322            }
323            _ => return None,
324        };
325        char::from_u32(code)
326    }
327
328    /// The grapheme the cell shows; empty for a continuation.
329    pub fn text(&self) -> &str {
330        if self.len == LONG {
331            let at = u32::from_le_bytes(self.bytes) as usize;
332            let table = graphemes().read().unwrap_or_else(|e| e.into_inner());
333            return table.texts[at];
334        }
335        // Written from a `&str` or a `char`, so the bytes are UTF-8.
336        std::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or(" ")
337    }
338
339    pub fn is_continuation(&self) -> bool {
340        self.width == 0
341    }
342
343    /// Whether the cell's text is empty (a wide character's second half).
344    #[inline]
345    pub(crate) fn is_empty_text(&self) -> bool {
346        self.len == 0
347    }
348
349    #[inline]
350    fn set(&mut self, text: &str, width: u8, style: StyleId) {
351        *self = Cell::new(text, width, style);
352    }
353
354    /// The same grapheme, `width` wide, in `style`.
355    #[inline]
356    fn restyled(self, width: u8, style: StyleId) -> Cell {
357        Cell {
358            width,
359            style,
360            ..self
361        }
362    }
363}
364
365/// A run of text in one style, for rs-rich-ext's fast path
366/// ([`Screen::write_runs`]): the text borrowed, the style an id already
367/// interned in the screen written to. rich's [`Segment`] owns its text and
368/// its [`Style`]; a run owns neither, so a line of runs costs no
369/// allocation a run.
370#[derive(Clone, Copy, Debug, PartialEq, Eq)]
371pub struct TextRun<'a> {
372    pub text: &'a str,
373    pub style: StyleId,
374}
375
376impl<'a> TextRun<'a> {
377    pub fn new(text: &'a str, style: StyleId) -> TextRun<'a> {
378        TextRun { text, style }
379    }
380}
381
382/// The grid of cells, and the styles they use.
383///
384/// The style table only grows as cells are written; a screen kept between
385/// frames drops the styles no cell uses any more with
386/// [`compact_styles`](Self::compact_styles) (a [`Painter`] does it for the
387/// screen it paints with [`Painter::compact`]), so a picture whose colours
388/// change every frame does not pile them up.
389#[derive(Clone, Debug)]
390pub struct Screen {
391    width: u16,
392    height: u16,
393    cells: Vec<Cell>,
394    styles: StyleTable,
395    /// The style last laid under or over cells, and its id.
396    layer: Option<(Style, StyleId)>,
397    /// What laying a style under or over a cell's made: by (layer, own,
398    /// over), the combined style's id.
399    layered: HashMap<(u32, u32, bool), StyleId, FxBuild>,
400    /// The rows [`scroll_up`](Self::scroll_up) moved since
401    /// [`forget_scrolls`](Self::forget_scrolls), newest last (a few): what
402    /// a painter tries first when it looks for rows that moved.
403    scrolled: Vec<(Rect, u16)>,
404    /// Scratch for writing lines, kept so a frame allocates none: each
405    /// segment's style id, and a run's graphemes.
406    scratch_ids: Vec<StyleId>,
407    scratch_spans: Vec<(usize, usize, usize)>,
408}
409
410impl Screen {
411    pub fn new(width: u16, height: u16) -> Screen {
412        Screen {
413            width,
414            height,
415            cells: vec![Cell::blank(); width as usize * height as usize],
416            styles: StyleTable::new(),
417            layer: None,
418            layered: HashMap::default(),
419            scrolled: Vec::new(),
420            scratch_ids: Vec::new(),
421            scratch_spans: Vec::new(),
422        }
423    }
424
425    /// The scrolls [`scroll_up`](Self::scroll_up) made since the last
426    /// [`forget_scrolls`](Self::forget_scrolls): each rectangle and how
427    /// many rows it moved up.
428    pub fn scrolled(&self) -> &[(Rect, u16)] {
429        &self.scrolled
430    }
431
432    /// Forget the scrolls made so far: after a frame is painted.
433    pub fn forget_scrolls(&mut self) {
434        self.scrolled.clear();
435    }
436
437    /// Row `y`'s cells.
438    fn row(&self, y: u16) -> &[Cell] {
439        let width = self.width as usize;
440        &self.cells[y as usize * width..(y as usize + 1) * width]
441    }
442
443    pub fn area(&self) -> Rect {
444        Rect::new(0, 0, self.width, self.height)
445    }
446
447    pub fn cell(&self, x: u16, y: u16) -> &Cell {
448        &self.cells[y as usize * self.width as usize + x as usize]
449    }
450
451    /// The cell at `x`, `y`, if it is on the screen; its style is
452    /// [`style`](Self::style) of its id.
453    ///
454    /// ```
455    /// use rich::{Segment, Style};
456    /// use rich_ext::surface::{Rect, Screen};
457    ///
458    /// let mut screen = Screen::new(4, 1);
459    /// let bold = Style::parse("bold").unwrap();
460    /// screen.write_lines(Rect::new(0, 0, 4, 1), &[vec![Segment::new("ab", Some(bold.clone()))]]);
461    /// let cell = screen.get(1, 0).unwrap();
462    /// assert_eq!((cell.text(), screen.style(cell.style)), ("b", Some(&bold)));
463    /// assert!(screen.get(4, 0).is_none());
464    /// ```
465    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
466        (x < self.width && y < self.height).then(|| self.cell(x, y))
467    }
468
469    fn cell_mut(&mut self, x: u16, y: u16) -> &mut Cell {
470        let width = self.width as usize;
471        &mut self.cells[y as usize * width + x as usize]
472    }
473
474    pub fn style(&self, id: StyleId) -> Option<&Style> {
475        self.styles.get(id)
476    }
477
478    /// How many styles the screen's table holds, counting "no style":
479    /// what its cells use, and what they used before the last
480    /// [`compact_styles`](Self::compact_styles).
481    pub fn style_count(&self) -> usize {
482        self.styles.len()
483    }
484
485    /// Intern `style` for use in this screen's cells.
486    pub fn intern(&mut self, style: Option<&Style>) -> StyleId {
487        self.styles.intern(style)
488    }
489
490    /// Whether the style table has grown well past what the cells used
491    /// when it was last compacted: time for
492    /// [`compact_styles`](Self::compact_styles).
493    pub fn styles_piled_up(&self) -> bool {
494        self.styles.piled_up()
495    }
496
497    /// Drop the styles no cell uses, and renumber the rest. The result maps
498    /// the old ids to the new, for whatever kept an id of this screen's
499    /// between frames: a [`Painter`] that painted it is moved over with
500    /// [`Painter::remap_styles`] (or compacts it itself with
501    /// [`Painter::compact`]). The cells keep their styles; only the ids
502    /// change.
503    ///
504    /// ```
505    /// use rich::{Color, Style};
506    /// use rich_ext::surface::Screen;
507    ///
508    /// let mut screen = Screen::new(2, 1);
509    /// for n in 0..=255u8 {
510    ///     screen.put_rgb(0, 0, '▀', Some([n, 0, 0]), Some([0, 0, n]));
511    /// }
512    /// assert_eq!(screen.style_count(), 257);
513    /// screen.compact_styles();
514    /// // "No style" (the blank cell) and the last colour pair.
515    /// assert_eq!(screen.style_count(), 2);
516    /// let cell = screen.cell(0, 0);
517    /// let last = Style::from_color(Some(Color::from_rgb(255, 0, 0)), Some(Color::from_rgb(0, 0, 255)));
518    /// assert_eq!(screen.style(cell.style), Some(&last));
519    /// ```
520    pub fn compact_styles(&mut self) -> StyleRemap {
521        let mut keep = vec![false; self.styles.len()];
522        for cell in &self.cells {
523            if let Some(used) = keep.get_mut(cell.style.index()) {
524                *used = true;
525            }
526        }
527        let remap = self.styles.retain(&keep);
528        for cell in &mut self.cells {
529            cell.style = remap.get_or_gone(cell.style);
530        }
531        self.layer = None;
532        self.layered.clear();
533        remap
534    }
535
536    /// Set the cell at `x`, `y` to `ch` with foreground `fg` and background
537    /// `bg` in true colour (either missing), without building a
538    /// [`Segment`] or, unless the pair is new, a [`Style`]: the fast way to
539    /// draw a picture a cell at a time (half blocks, quadrants, Braille).
540    /// The cell takes the style [`Style::from_color`] of the two
541    /// [`Color::from_rgb`](rich::Color::from_rgb)s would give. Off the
542    /// screen, nothing is written.
543    ///
544    /// `ch` takes one cell: a wide or zero-width character becomes a space,
545    /// and a terminal control its visible stand-in (see
546    /// [`control_picture`](super::control_picture)). A wide grapheme the
547    /// cell was half of becomes a space.
548    ///
549    /// ```
550    /// use rich::{Color, Style};
551    /// use rich_ext::surface::Screen;
552    ///
553    /// let mut screen = Screen::new(2, 1);
554    /// screen.put_rgb(1, 0, '▀', Some([255, 0, 0]), Some([0, 0, 255]));
555    /// let cell = screen.cell(1, 0);
556    /// let style = Style::from_color(
557    ///     Some(Color::from_rgb(255, 0, 0)),
558    ///     Some(Color::from_rgb(0, 0, 255)),
559    /// );
560    /// assert_eq!((cell.text(), screen.style(cell.style)), ("▀", Some(&style)));
561    /// ```
562    pub fn put_rgb(&mut self, x: u16, y: u16, ch: char, fg: Option<Rgb>, bg: Option<Rgb>) {
563        if x >= self.width || y >= self.height {
564            return;
565        }
566        let id = self.styles.intern_rgb(fg, bg);
567        self.put_cell(x, y, ch, id);
568    }
569
570    /// Set the cell at `x`, `y` to `ch` in the style `style` (an id from
571    /// [`intern`](Self::intern) on this screen), as
572    /// [`put_rgb`](Self::put_rgb) does: for many cells in one style,
573    /// interned once.
574    ///
575    /// ```
576    /// use rich::Style;
577    /// use rich_ext::surface::Screen;
578    ///
579    /// let mut screen = Screen::new(3, 1);
580    /// let bold = screen.intern(Some(&Style::parse("bold").unwrap()));
581    /// for x in 0..3 {
582    ///     screen.put(x, 0, '#', bold);
583    /// }
584    /// assert_eq!(screen.plain(), ["###"]);
585    /// ```
586    pub fn put(&mut self, x: u16, y: u16, ch: char, style: StyleId) {
587        if x >= self.width
588            || y >= self.height
589            || self.styles.get(style).is_none() && style != StyleId::NONE
590        {
591            return;
592        }
593        self.put_cell(x, y, ch, style);
594    }
595
596    #[inline]
597    fn put_cell(&mut self, x: u16, y: u16, ch: char, style: StyleId) {
598        let ch = narrow(ch);
599        let at = y as usize * self.width as usize + x as usize;
600        let width = self.cells[at].width;
601        if width == 0 && x > 0 {
602            // The second column of a wide grapheme: its first is erased too.
603            let lead = &mut self.cells[at - 1];
604            *lead = Cell::char(' ', 1, lead.style);
605        } else if width >= 2 && x + 1 < self.width {
606            let next = &mut self.cells[at + 1];
607            *next = Cell::char(' ', 1, next.style);
608        }
609        self.cells[at] = Cell::char(ch, 1, style);
610    }
611
612    /// A wide grapheme that `rect` cuts in half, on either side, becomes a
613    /// space: the terminal erases a wide grapheme when either column is
614    /// written, so the half outside `rect` must not stay in the model.
615    fn split_edges(&mut self, rect: Rect) {
616        let rect = rect.intersection(self.area());
617        if rect.is_empty() {
618            return;
619        }
620        for y in rect.y..rect.bottom() {
621            if rect.x > 0 && self.cell(rect.x, y).is_continuation() {
622                let style = self.cell(rect.x - 1, y).style;
623                self.cell_mut(rect.x - 1, y).set(" ", 1, style);
624            }
625            if rect.right() < self.width && self.cell(rect.right(), y).is_continuation() {
626                let style = self.cell(rect.right(), y).style;
627                self.cell_mut(rect.right(), y).set(" ", 1, style);
628            }
629        }
630    }
631
632    /// Clear `rect` to blank cells.
633    pub fn clear(&mut self, rect: Rect) {
634        let rect = rect.intersection(self.area());
635        self.split_edges(rect);
636        for y in rect.y..rect.bottom() {
637            for x in rect.x..rect.right() {
638                *self.cell_mut(x, y) = Cell::blank();
639            }
640        }
641    }
642
643    /// Write rendered `lines` into `rect`, replacing what was there: rows
644    /// and columns the lines do not reach are cleared. Terminal controls in
645    /// the text become visible symbols; a wide grapheme that would cross
646    /// the rectangle's right edge becomes a space.
647    pub fn write_lines(&mut self, rect: Rect, lines: &[Vec<Segment>]) {
648        let rect = rect.intersection(self.area());
649        if rect.is_empty() {
650            return;
651        }
652        self.split_edges(rect);
653        if !self.write_simple_lines(rect, lines) {
654            self.write_frame_lines(rect, lines);
655        }
656    }
657
658    /// [`write_lines`](Self::write_lines) through a [`Frame`], for any
659    /// text: graphemes of several characters, controls, region tags.
660    fn write_frame_lines(&mut self, rect: Rect, lines: &[Vec<Segment>]) {
661        let mut segments = Vec::new();
662        for line in lines.iter().take(rect.height as usize) {
663            let start = segments.len();
664            segments.extend(line.iter().cloned());
665            super::sanitize_line(&mut segments[start..]);
666            segments.push(Segment::line());
667        }
668        let frame = Frame::from_segments(&segments);
669        // The frame's style ids, translated into this screen's table once.
670        let mut ids: Vec<Option<StyleId>> = vec![None; frame.styles().len() + 1];
671        for row in 0..rect.height {
672            let cells = if (row as usize) < frame.height() {
673                frame.cells(row as usize)
674            } else {
675                Vec::new()
676            };
677            let y = rect.y + row;
678            let mut x = rect.x;
679            for cell in &cells {
680                if x >= rect.right() {
681                    break;
682                }
683                if cell.is_continuation() {
684                    continue;
685                }
686                let id = *ids[cell.style.index()].get_or_insert_with(|| {
687                    // A style that sets nothing is no style.
688                    let style = frame.styles().get(cell.style).filter(|s| !s.is_null());
689                    self.styles.intern(style)
690                });
691                if cell.width >= 2 && x + 1 >= rect.right() {
692                    // No room for the second column.
693                    self.cell_mut(x, y).set(" ", 1, id);
694                    x += 1;
695                    continue;
696                }
697                self.cell_mut(x, y).set(cell.text, cell.width.max(1), id);
698                for extra in 1..cell.width {
699                    self.cell_mut(x + extra as u16, y).set("", 0, id);
700                }
701                x += cell.width.max(1) as u16;
702            }
703            while x < rect.right() {
704                *self.cell_mut(x, y) = Cell::blank();
705                x += 1;
706            }
707        }
708    }
709
710    /// [`write_lines`](Self::write_lines) without building a [`Frame`]:
711    /// segments go straight into cells, a character a cell where every
712    /// character is its own grapheme (most text, every picture), and
713    /// through rich's grapheme splitting where it is not (combining marks,
714    /// joined emoji). It keeps the Frame path's rules: a zero-width
715    /// grapheme joins the cell before it, even across segments, or the
716    /// row's first cell when nothing precedes it. Lines with a line break
717    /// inside a segment, or a region tag, are left to the Frame path:
718    /// whether it wrote them; when it did not, nothing was changed.
719    fn write_simple_lines(&mut self, rect: Rect, lines: &[Vec<Segment>]) -> bool {
720        let lines = &lines[..lines.len().min(rect.height as usize)];
721        let direct = lines.iter().flatten().all(|segment| {
722            segment.control
723                || segment
724                    .style
725                    .as_ref()
726                    .is_none_or(|s| s.meta_ref().is_none())
727                    && !segment.text.contains('\n')
728        });
729        if !direct {
730            return false;
731        }
732        let mut spans = std::mem::take(&mut self.scratch_spans);
733        let mut ids = std::mem::take(&mut self.scratch_ids);
734        for row in 0..rect.height {
735            let line = lines.get(row as usize).map_or(&[][..], Vec::as_slice);
736            ids.clear();
737            for segment in line {
738                // A style that sets nothing is no style.
739                let style = segment.style.as_ref().filter(|s| !s.is_null());
740                ids.push(if segment.control {
741                    StyleId::NONE
742                } else {
743                    self.styles.intern(style)
744                });
745            }
746            let pieces = line
747                .iter()
748                .zip(&ids)
749                .filter(|(segment, _)| !segment.control)
750                .map(|(segment, &id)| (segment.text.as_str(), id));
751            self.write_row(rect, rect.y + row, pieces, &mut spans);
752        }
753        self.scratch_spans = spans;
754        self.scratch_ids = ids;
755        true
756    }
757
758    /// The fast path's [`write_lines`](Self::write_lines): rows of
759    /// [`TextRun`]s, each a borrowed text and a style already interned in
760    /// this screen, written into `rect` as `write_lines` writes the same
761    /// text in the same styles, without a [`Segment`] or a [`Style`] a run.
762    /// Rows and columns the runs do not reach are cleared. A run with a
763    /// line break falls back to `write_lines` (its text cannot be one row).
764    ///
765    /// ```
766    /// use rich::{Segment, Style};
767    /// use rich_ext::frame::StyleId;
768    /// use rich_ext::surface::{Rect, Screen, TextRun};
769    ///
770    /// let mut fast = Screen::new(6, 1);
771    /// let bold = fast.intern(Some(&Style::parse("bold").unwrap()));
772    /// fast.write_runs(
773    ///     Rect::new(0, 0, 6, 1),
774    ///     &[vec![TextRun::new("ab", bold), TextRun::new("c", StyleId::NONE)]],
775    /// );
776    /// // The same cells as rich's path.
777    /// let mut slow = Screen::new(6, 1);
778    /// slow.write_lines(
779    ///     Rect::new(0, 0, 6, 1),
780    ///     &[vec![Segment::new("ab", Some(Style::parse("bold").unwrap())), Segment::new("c", None)]],
781    /// );
782    /// assert_eq!(fast.lines(), slow.lines());
783    /// ```
784    pub fn write_runs<'t, L: AsRef<[TextRun<'t>]>>(&mut self, rect: Rect, lines: &[L]) {
785        let rect = rect.intersection(self.area());
786        if rect.is_empty() {
787            return;
788        }
789        let lines = &lines[..lines.len().min(rect.height as usize)];
790        // What only the Frame path writes: a line break inside a run, or a
791        // style with metadata (region tags).
792        let mut checked = None;
793        let framed = lines.iter().flat_map(AsRef::as_ref).any(|run| {
794            run.text.contains('\n')
795                || checked != Some(run.style) && {
796                    checked = Some(run.style);
797                    self.styles
798                        .get(run.style)
799                        .is_some_and(|s| s.meta_ref().is_some())
800                }
801        });
802        if framed {
803            let segments: Vec<Vec<Segment>> = lines
804                .iter()
805                .map(|line| {
806                    line.as_ref()
807                        .iter()
808                        .map(|run| Segment::new(run.text, self.style(run.style).cloned()))
809                        .collect()
810                })
811                .collect();
812            self.write_lines(rect, &segments);
813            return;
814        }
815        self.split_edges(rect);
816        let mut spans = std::mem::take(&mut self.scratch_spans);
817        let mut ids = std::mem::take(&mut self.scratch_ids);
818        for row in 0..rect.height {
819            let line = lines.get(row as usize).map_or(&[][..], AsRef::as_ref);
820            ids.clear();
821            // A style that sets nothing is no style, as for segments.
822            ids.extend(line.iter().map(|run| match self.styles.get(run.style) {
823                Some(style) if style.is_null() => StyleId::NONE,
824                _ => run.style,
825            }));
826            let pieces = line.iter().zip(&ids).map(|(run, &id)| (run.text, id));
827            self.write_row(rect, rect.y + row, pieces, &mut spans);
828        }
829        self.scratch_spans = spans;
830        self.scratch_ids = ids;
831    }
832
833    /// The fast path for a grid of cells, a chart's: `cells`, `width` a
834    /// row and `rows` rows, each empty (a space with no style) or a cell
835    /// whose style is an index into `ids`, the screen's style for it,
836    /// copied into `rect` as [`write_runs`](Self::write_runs) would write
837    /// the same text. Only a grid of single characters is taken (no
838    /// controls, zero-width marks or long graphemes, every wide character
839    /// followed by its empty second half); for any other nothing is
840    /// written and the result is false.
841    pub(crate) fn write_cell_grid(
842        &mut self,
843        rect: Rect,
844        width: usize,
845        rows: usize,
846        cells: &[Option<Cell>],
847        ids: &[StyleId],
848    ) -> bool {
849        let rect = rect.intersection(self.area());
850        if rect.is_empty() {
851            return true;
852        }
853        let rows = rows.min(rect.height as usize);
854        let cells = &cells[..rows * width];
855        for line in cells.chunks_exact(width.max(1)) {
856            let mut wide = false;
857            for (at, cell) in line.iter().enumerate() {
858                let Some(cell) = cell else {
859                    if wide {
860                        return false;
861                    }
862                    continue;
863                };
864                if cell.len == 0 {
865                    // The second half of a wide character, and only that.
866                    if !wide {
867                        return false;
868                    }
869                    wide = false;
870                    continue;
871                }
872                if wide {
873                    return false;
874                }
875                if cell.len == 1 {
876                    if !(b' '..=b'~').contains(&cell.bytes[0]) || cell.width != 1 {
877                        return false;
878                    }
879                    continue;
880                }
881                let Some(c) = cell.single_char() else {
882                    return false;
883                };
884                // Braille, blocks and box drawing: one column, no controls.
885                if matches!(c, '\u{2500}'..='\u{259f}' | '\u{2800}'..='\u{28ff}') && cell.width == 1
886                {
887                    continue;
888                }
889                let w = char_width(c);
890                if super::control_picture(c).is_some()
891                    || !matches!(w, 1 | 2)
892                    || c == '\u{200d}'
893                    || c == '\u{fe0f}'
894                    || usize::from(cell.width) != w
895                {
896                    return false;
897                }
898                wide = w == 2;
899                if wide && at + 1 == line.len() {
900                    return false;
901                }
902            }
903            if wide {
904                return false;
905            }
906        }
907        self.split_edges(rect);
908        let shown = width.min(rect.width as usize);
909        for row in 0..rect.height {
910            let y = rect.y + row;
911            let mut x = 0usize;
912            if (row as usize) < rows {
913                let line = &cells[row as usize * width..row as usize * width + shown];
914                while x < shown {
915                    let at = y as usize * self.width as usize + rect.x as usize + x;
916                    match line[x] {
917                        None => {
918                            self.cells[at] = Cell::blank();
919                            x += 1;
920                        }
921                        Some(cell) => {
922                            let id = ids[cell.style.index()];
923                            if cell.width == 2 {
924                                if x + 1 >= shown {
925                                    // No room for the second column.
926                                    self.cells[at] = Cell::char(' ', 1, id);
927                                    x += 1;
928                                    break;
929                                }
930                                self.cells[at] = cell.restyled(2, id);
931                                self.cells[at + 1] = Cell::new("", 0, id);
932                                x += 2;
933                            } else {
934                                self.cells[at] = cell.restyled(1, id);
935                                x += 1;
936                            }
937                        }
938                    }
939                }
940            }
941            for gx in x..rect.width as usize {
942                let at = y as usize * self.width as usize + rect.x as usize + gx;
943                self.cells[at] = Cell::blank();
944            }
945        }
946        true
947    }
948
949    /// One row of [`write_lines`](Self::write_lines) and
950    /// [`write_runs`](Self::write_runs): `pieces` of text, each in a style
951    /// of this screen, from the left of `rect` on row `y`; the rest of the
952    /// row is cleared.
953    fn write_row<'t>(
954        &mut self,
955        rect: Rect,
956        y: u16,
957        pieces: impl Iterator<Item = (&'t str, StyleId)>,
958        spans: &mut Vec<(usize, usize, usize)>,
959    ) {
960        let mut x = rect.x;
961        // What a zero-width grapheme joins: the cell last written, or,
962        // before any, the row's first (`lead` gathers them); none once
963        // the row is cut at the edge.
964        let mut last: Option<u16> = None;
965        let mut lead = String::new();
966        let mut cut = false;
967        for (text, id) in pieces {
968            // One look at the text: ASCII printables pass at once.
969            let (mut simple, mut controls) = (true, false);
970            for c in text.chars() {
971                if (' '..='~').contains(&c) {
972                    continue;
973                }
974                if super::control_picture(c).is_some() {
975                    controls = true;
976                } else if !matches!(char_width(c), 1 | 2) || c == '\u{200d}' || c == '\u{fe0f}' {
977                    simple = false;
978                }
979            }
980            if simple && !controls {
981                // A character a grapheme: straight into cells.
982                let mut chars = text.chars();
983                if !lead.is_empty()
984                    && !cut
985                    && let Some(c) = chars.next()
986                {
987                    lead.push(c);
988                    let piece = std::mem::take(&mut lead);
989                    self.put_grapheme(
990                        rect,
991                        &mut x,
992                        y,
993                        &piece,
994                        char_width(c),
995                        id,
996                        &mut last,
997                        &mut cut,
998                    );
999                }
1000                for c in chars {
1001                    if cut {
1002                        break;
1003                    }
1004                    if x >= rect.right() {
1005                        cut = true;
1006                        break;
1007                    }
1008                    let width = char_width(c) as u16;
1009                    if width == 2 && x + 1 >= rect.right() {
1010                        self.cell_mut(x, y).set(" ", 1, id);
1011                        x += 1;
1012                        cut = true;
1013                        continue;
1014                    }
1015                    *self.cell_mut(x, y) = Cell::char(c, width as u8, id);
1016                    if width == 2 {
1017                        self.cell_mut(x + 1, y).set("", 0, id);
1018                    }
1019                    last = Some(x);
1020                    x += width;
1021                }
1022                continue;
1023            }
1024            let sanitized;
1025            let text = if controls {
1026                sanitized = text
1027                    .chars()
1028                    .map(|c| super::control_picture(c).unwrap_or(c))
1029                    .collect::<String>();
1030                sanitized.as_str()
1031            } else {
1032                text
1033            };
1034            spans.clear();
1035            spans.extend(rich::cells::split_graphemes(text).0);
1036            for &(from, to, width) in spans.iter() {
1037                let piece = &text[from..to];
1038                if width == 0 {
1039                    if cut {
1040                        continue;
1041                    }
1042                    match last {
1043                        Some(at) => {
1044                            let cell = *self.cell(at, y);
1045                            let joined = format!("{}{piece}", cell.text());
1046                            *self.cell_mut(at, y) = Cell::new(&joined, cell.width, cell.style);
1047                        }
1048                        None => lead.push_str(piece),
1049                    }
1050                    continue;
1051                }
1052                if !lead.is_empty() && !cut {
1053                    let piece = std::mem::take(&mut lead) + piece;
1054                    self.put_grapheme(rect, &mut x, y, &piece, width, id, &mut last, &mut cut);
1055                } else {
1056                    self.put_grapheme(rect, &mut x, y, piece, width, id, &mut last, &mut cut);
1057                }
1058            }
1059        }
1060        while x < rect.right() {
1061            *self.cell_mut(x, y) = Cell::blank();
1062            x += 1;
1063        }
1064    }
1065
1066    /// One grapheme `width` cells wide at `x` of row `y`, for
1067    /// [`write_row`](Self::write_row): cut to a space at
1068    /// the rectangle's edge, nothing past it.
1069    #[allow(clippy::too_many_arguments)]
1070    fn put_grapheme(
1071        &mut self,
1072        rect: Rect,
1073        x: &mut u16,
1074        y: u16,
1075        piece: &str,
1076        width: usize,
1077        id: StyleId,
1078        last: &mut Option<u16>,
1079        cut: &mut bool,
1080    ) {
1081        if *cut {
1082            return;
1083        }
1084        if *x >= rect.right() {
1085            *cut = true;
1086            return;
1087        }
1088        let width = width as u16;
1089        if width >= 2 && *x + 1 >= rect.right() {
1090            // No room for the second column.
1091            self.cell_mut(*x, y).set(" ", 1, id);
1092            *x += 1;
1093            *cut = true;
1094            return;
1095        }
1096        *self.cell_mut(*x, y) = Cell::new(piece, width as u8, id);
1097        for extra in 1..width {
1098            self.cell_mut(*x + extra, y).set("", 0, id);
1099        }
1100        *last = Some(*x);
1101        *x += width;
1102    }
1103
1104    /// Lay `style` under the cell at `x`, `y`: it takes what the cell's own
1105    /// style leaves unset (a stylesheet's colours, as CSS inherits them).
1106    pub fn underlay(&mut self, x: u16, y: u16, style: &Style) {
1107        self.lay(x, y, style, false);
1108    }
1109
1110    /// Lay `style` over the cell at `x`, `y` (a selection's highlight).
1111    pub fn restyle(&mut self, x: u16, y: u16, style: &Style) {
1112        self.lay(x, y, style, true);
1113    }
1114
1115    /// Combine `style` with the cell's own, under it or over it. A style is
1116    /// usually laid over many cells of a few styles, so each combination is
1117    /// made and interned once.
1118    fn lay(&mut self, x: u16, y: u16, style: &Style, over: bool) {
1119        if x >= self.width || y >= self.height {
1120            return;
1121        }
1122        let layer = match &self.layer {
1123            Some((last, id)) if last == style => *id,
1124            _ => {
1125                let id = self.styles.intern(Some(style));
1126                self.layer = Some((style.clone(), id));
1127                id
1128            }
1129        };
1130        let own = self.cell(x, y).style;
1131        let key = (layer.index() as u32, own.index() as u32, over);
1132        let id = match self.layered.get(&key) {
1133            Some(&id) => id,
1134            None => {
1135                let combined = match self.styles.get(own) {
1136                    Some(own) if over => own.combine(style),
1137                    Some(own) => style.combine(own),
1138                    None => style.clone(),
1139                };
1140                let id = self.styles.intern(Some(&combined));
1141                self.layered.insert(key, id);
1142                id
1143            }
1144        };
1145        self.cell_mut(x, y).style = id;
1146    }
1147
1148    /// Copy the cells of `from` in `source` to this screen at `x`, `y`,
1149    /// clipped to this screen. A wide character cut by either edge of
1150    /// `from`, or by this screen's right edge, becomes a space.
1151    pub fn blit(&mut self, source: &Screen, from: Rect, x: u16, y: u16) {
1152        let from = from.intersection(source.area());
1153        self.split_edges(Rect::new(x, y, from.width, from.height));
1154        // The source's style ids, translated into this screen's table once.
1155        let mut ids: Vec<Option<StyleId>> = vec![None; source.styles.len()];
1156        for row in 0..from.height {
1157            let ty = y.saturating_add(row);
1158            if ty >= self.height {
1159                break;
1160            }
1161            for column in 0..from.width {
1162                let tx = x.saturating_add(column);
1163                if tx >= self.width {
1164                    break;
1165                }
1166                let cell = *source.cell(from.x + column, from.y + row);
1167                let cut = cell.is_continuation() && column == 0
1168                    || cell.width >= 2 && (column + 1 >= from.width || tx + 1 >= self.width);
1169                let styles = &mut self.styles;
1170                let id = match ids.get_mut(cell.style.index()) {
1171                    Some(slot) => {
1172                        *slot.get_or_insert_with(|| styles.intern(source.style(cell.style)))
1173                    }
1174                    None => styles.intern(source.style(cell.style)),
1175                };
1176                if cut {
1177                    self.cell_mut(tx, ty).set(" ", 1, id);
1178                } else {
1179                    *self.cell_mut(tx, ty) = cell.restyled(cell.width, id);
1180                }
1181            }
1182        }
1183    }
1184
1185    /// Move the rows of `rect` up by `rows`, dropping the top ones; the
1186    /// rows that open at the bottom are cleared.
1187    pub fn scroll_up(&mut self, rect: Rect, rows: u16) {
1188        let rect = rect.intersection(self.area());
1189        if rect.is_empty() {
1190            return;
1191        }
1192        let rows = rows.min(rect.height);
1193        if self.scrolled.len() >= 8 {
1194            self.scrolled.remove(0);
1195        }
1196        self.scrolled.push((rect, rows));
1197        self.split_edges(rect);
1198        let width = self.width as usize;
1199        for y in rect.y..rect.bottom() - rows {
1200            let (from, to) = ((y + rows) as usize * width, y as usize * width);
1201            let (a, b) = (from + rect.x as usize, from + rect.right() as usize);
1202            self.cells.copy_within(a..b, to + rect.x as usize);
1203        }
1204        self.clear(Rect::new(rect.x, rect.bottom() - rows, rect.width, rows));
1205    }
1206
1207    /// Each row as rendered segments, a run of cells in one style per
1208    /// segment: what any rich renderer takes, so a frame can be drawn
1209    /// somewhere else (into a ratatui buffer with `rs-rich-ratatui`'s
1210    /// `lines_to_buffer`, for one).
1211    pub fn lines(&self) -> Vec<Vec<Segment>> {
1212        (0..self.height)
1213            .map(|y| {
1214                let mut line: Vec<Segment> = Vec::new();
1215                let mut run = String::new();
1216                let mut style = None;
1217                for x in 0..self.width {
1218                    let cell = self.cell(x, y);
1219                    if cell.is_continuation() {
1220                        continue;
1221                    }
1222                    if style != Some(cell.style) && !run.is_empty() {
1223                        let id = style.expect("a run has a style");
1224                        line.push(Segment::new(
1225                            std::mem::take(&mut run),
1226                            self.style(id).cloned(),
1227                        ));
1228                    }
1229                    style = Some(cell.style);
1230                    run.push_str(cell.text());
1231                }
1232                if let Some(id) = style.filter(|_| !run.is_empty()) {
1233                    line.push(Segment::new(run, self.style(id).cloned()));
1234                }
1235                line
1236            })
1237            .collect()
1238    }
1239
1240    /// The screen as an SVG picture of a terminal, through
1241    /// [`Frame::to_svg`]: each row's runs of cells in their styles, as wide
1242    /// as the screen. A screenshot of an app, or a snapshot to review.
1243    ///
1244    /// ```
1245    /// use rich::{Segment, Style};
1246    /// use rich_ext::frame::SvgOptions;
1247    /// use rich_ext::surface::{Rect, Screen};
1248    ///
1249    /// let mut screen = Screen::new(20, 3);
1250    /// let red = Style::parse("bold red").unwrap();
1251    /// screen.write_lines(Rect::new(0, 0, 20, 1), &[vec![Segment::new("hi", Some(red))]]);
1252    /// let svg = screen.to_svg(&SvgOptions::default().title("demo"));
1253    /// assert!(svg.starts_with("<svg"));
1254    /// assert!(svg.contains("font-weight: bold"));
1255    /// ```
1256    pub fn to_svg(&self, options: &crate::frame::SvgOptions<'_>) -> String {
1257        let mut segments = Vec::new();
1258        for (y, line) in self.lines().into_iter().enumerate() {
1259            if y > 0 {
1260                segments.push(Segment::line());
1261            }
1262            segments.extend(line);
1263        }
1264        let mut options = options.clone();
1265        options.width.get_or_insert(self.width as usize);
1266        Frame::from_segments(&segments).to_svg(&options)
1267    }
1268
1269    /// Each row as plain text (continuation cells skipped).
1270    pub fn plain(&self) -> Vec<String> {
1271        (0..self.height)
1272            .map(|y| {
1273                (0..self.width)
1274                    .map(|x| self.cell(x, y))
1275                    .filter(|c| !c.is_continuation())
1276                    .map(|c| c.text())
1277                    .collect()
1278            })
1279            .collect()
1280    }
1281}
1282
1283/// The cells `c` takes, as rich measures it. rich answers the characters
1284/// pictures and most text are made of (ASCII, Latin letters, box drawing,
1285/// blocks, Braille, CJK ideographs, Hangul) without its table search, the
1286/// same in every Unicode version its tables cover.
1287#[inline]
1288pub(crate) fn char_width(c: char) -> usize {
1289    rich::cells::char_cell_width(c)
1290}
1291
1292/// One cell wide: `ch`, or a space for a wide or zero-width character, or
1293/// a control's visible stand-in.
1294#[inline]
1295fn narrow(ch: char) -> char {
1296    // ASCII, box drawing and block elements, Braille: the pictures' cells.
1297    if (' '..='~').contains(&ch)
1298        || ('\u{2500}'..='\u{259f}').contains(&ch)
1299        || ('\u{2800}'..='\u{28ff}').contains(&ch)
1300    {
1301        return ch;
1302    }
1303    if let Some(picture) = super::control_picture(ch) {
1304        return picture;
1305    }
1306    let mut buffer = [0u8; 4];
1307    if rich::cells::cell_len(ch.encode_utf8(&mut buffer)) == 1 {
1308        ch
1309    } else {
1310        ' '
1311    }
1312}
1313
1314/// What a [`Painter`] last sent: the cells, by the style ids of the screen
1315/// it painted (whose table it does not copy).
1316struct Sent {
1317    width: u16,
1318    height: u16,
1319    cells: Vec<Cell>,
1320}
1321
1322impl Sent {
1323    fn row(&self, y: u16) -> &[Cell] {
1324        let width = self.width as usize;
1325        &self.cells[y as usize * width..(y as usize + 1) * width]
1326    }
1327
1328    /// Move rows `top`..`bottom` by `by` (up when positive), as the
1329    /// terminal did: the rows it opened are blank.
1330    fn shift(&mut self, top: u16, bottom: u16, by: i32) {
1331        let width = self.width as usize;
1332        let rows = (bottom - top) as i32;
1333        let n = by.unsigned_abs() as u16;
1334        if by > 0 {
1335            for y in top..bottom - n {
1336                let from = (y + n) as usize * width;
1337                self.cells
1338                    .copy_within(from..from + width, y as usize * width);
1339            }
1340        } else {
1341            for y in (top + n..bottom).rev() {
1342                let from = (y - n) as usize * width;
1343                self.cells
1344                    .copy_within(from..from + width, y as usize * width);
1345            }
1346        }
1347        let opened = if by > 0 {
1348            bottom - n..bottom
1349        } else {
1350            top..top + n
1351        };
1352        debug_assert!(rows > by.abs());
1353        for y in opened {
1354            let row = y as usize * width;
1355            self.cells[row..row + width].fill(Cell::blank());
1356        }
1357    }
1358}
1359
1360/// How far from its row a changed row is looked for among the rows sent,
1361/// to find a move.
1362const SCROLL_WINDOW: u16 = 32;
1363
1364/// Rows `top`..`bottom` that moved together by `by` rows (up when
1365/// positive) since they were sent.
1366#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1367struct Shift {
1368    top: u16,
1369    bottom: u16,
1370    by: i32,
1371    /// Rows the move puts right that were wrong.
1372    gain: u16,
1373}
1374
1375/// How a [`Painter`] turns a frame's changes into bytes.
1376///
1377/// Every strategy draws the same screen; they differ in the bytes they
1378/// send and the time they take to find them. The benchmark in
1379/// rs-rich-intuituive's `tests/paint_bench.rs` measures each on a set of
1380/// scenes, and `docs/design/render-performance.md` keeps its table.
1381///
1382/// ```
1383/// use rich_ext::surface::PaintStrategy;
1384///
1385/// assert_eq!("best".parse::<PaintStrategy>(), Ok(PaintStrategy::Best));
1386/// assert_eq!(PaintStrategy::default(), PaintStrategy::Best);
1387/// assert_eq!(PaintStrategy::Scroll.name(), "scroll");
1388/// assert!("fastest".parse::<PaintStrategy>().is_err());
1389/// ```
1390#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1391pub enum PaintStrategy {
1392    /// Compare each cell inside the damage with what was sent, and write
1393    /// the cells that changed. A bare [`Painter`]'s strategy.
1394    Cells,
1395    /// Compare whole rows, and rewrite each
1396    /// changed row from its first change to its last.
1397    Rows,
1398    /// Write everything: each row in turn, the rest of the screen erased.
1399    Full,
1400    /// Find runs of rows that moved together (a log's new line, a list
1401    /// scrolled), move them with the terminal's own scrolling, then write
1402    /// the cells that still differ. Off where scrolling is not safe (a
1403    /// Windows console, an inline region, an image in the moved rows).
1404    Scroll,
1405    /// Each frame, encode the strategies that apply and send the shortest,
1406    /// within a bound so choosing never costs more than it saves. The
1407    /// default, and an intuiTUIve app's unless it chooses: within 5% of
1408    /// the best fixed strategy on every scene of the benchmark.
1409    #[default]
1410    Best,
1411}
1412
1413impl PaintStrategy {
1414    /// Every strategy, in the order of their names.
1415    pub const ALL: [PaintStrategy; 5] = [
1416        PaintStrategy::Cells,
1417        PaintStrategy::Rows,
1418        PaintStrategy::Full,
1419        PaintStrategy::Scroll,
1420        PaintStrategy::Best,
1421    ];
1422
1423    /// The strategy's name: `cells`, `rows`, `full`, `scroll` or `best`.
1424    pub fn name(self) -> &'static str {
1425        match self {
1426            PaintStrategy::Cells => "cells",
1427            PaintStrategy::Rows => "rows",
1428            PaintStrategy::Full => "full",
1429            PaintStrategy::Scroll => "scroll",
1430            PaintStrategy::Best => "best",
1431        }
1432    }
1433
1434    /// The strategy `RICH_PAINT_STRATEGY` names, if it is set to one.
1435    pub fn from_env() -> Option<PaintStrategy> {
1436        std::env::var("RICH_PAINT_STRATEGY")
1437            .ok()?
1438            .trim()
1439            .parse()
1440            .ok()
1441    }
1442}
1443
1444impl std::fmt::Display for PaintStrategy {
1445    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1446        f.write_str(self.name())
1447    }
1448}
1449
1450impl std::str::FromStr for PaintStrategy {
1451    type Err = String;
1452
1453    /// A strategy by its [name](PaintStrategy::name), in any case.
1454    fn from_str(name: &str) -> Result<PaintStrategy, String> {
1455        PaintStrategy::ALL
1456            .into_iter()
1457            .find(|s| s.name().eq_ignore_ascii_case(name))
1458            .ok_or_else(|| format!("no paint strategy {name:?}: cells, rows, full, scroll or best"))
1459    }
1460}
1461
1462/// What the last [`Painter::paint`] did, counted as notcurses counts a
1463/// render: the cells it looked at and wrote, and its bytes by kind. The
1464/// bytes add up to the length of what it returned.
1465///
1466/// ```
1467/// use rich::{ColorSystem, Segment};
1468/// use rich_ext::surface::{Painter, Rect, Screen};
1469///
1470/// let mut screen = Screen::new(10, 1);
1471/// let mut painter = Painter::new(Some(ColorSystem::Truecolor));
1472/// painter.paint(&screen, &[]);
1473/// screen.write_lines(screen.area(), &[vec![Segment::new("hi", None)]]);
1474/// let out = painter.paint(&screen, &[Rect::new(0, 0, 4, 1)]);
1475/// let stats = painter.stats();
1476/// // The damage, and a column to its right: two cells changed.
1477/// assert_eq!((stats.compared, stats.written, stats.elided), (5, 2, 3));
1478/// assert_eq!(stats.text_bytes, 2);
1479/// assert_eq!(stats.bytes(), out.len());
1480/// ```
1481#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1482pub struct PaintStats {
1483    /// Cells compared with what was sent (every cell, on a full repaint).
1484    pub compared: usize,
1485    /// Cells written.
1486    pub written: usize,
1487    /// Cells compared and found unchanged, so not written.
1488    pub elided: usize,
1489    /// Bytes of cursor moves.
1490    pub move_bytes: usize,
1491    /// Bytes of SGR styles and hyperlinks.
1492    pub style_bytes: usize,
1493    /// Bytes of the cells' text.
1494    pub text_bytes: usize,
1495    /// Everything else: clearing the screen, erasing, scrolling.
1496    pub other_bytes: usize,
1497    /// The strategy the paint used (never [`PaintStrategy::Best`], which
1498    /// picks one of the others).
1499    pub strategy: PaintStrategy,
1500}
1501
1502impl Default for PaintStats {
1503    /// Nothing counted, under [`PaintStrategy::Cells`] (`strategy` is never
1504    /// `Best`).
1505    fn default() -> PaintStats {
1506        PaintStats {
1507            compared: 0,
1508            written: 0,
1509            elided: 0,
1510            move_bytes: 0,
1511            style_bytes: 0,
1512            text_bytes: 0,
1513            other_bytes: 0,
1514            strategy: PaintStrategy::Cells,
1515        }
1516    }
1517}
1518
1519impl PaintStats {
1520    /// All the bytes the paint wrote.
1521    pub fn bytes(&self) -> usize {
1522        self.move_bytes + self.style_bytes + self.text_bytes + self.other_bytes
1523    }
1524
1525    /// Add `other`'s counts to these.
1526    pub fn add(&mut self, other: &PaintStats) {
1527        self.compared += other.compared;
1528        self.written += other.written;
1529        self.elided += other.elided;
1530        self.move_bytes += other.move_bytes;
1531        self.style_bytes += other.style_bytes;
1532        self.text_bytes += other.text_bytes;
1533        self.other_bytes += other.other_bytes;
1534        self.strategy = other.strategy;
1535    }
1536}
1537
1538/// Turns screens into terminal output: the cells that changed since the
1539/// last paint, inside the damaged rectangles.
1540///
1541/// It keeps a copy of the cells it sent, not of the style table: cells
1542/// compare by style id, so a screen's ids must not change under it, except
1543/// through [`compact`](Self::compact) or [`remap_styles`](Self::remap_styles).
1544pub struct Painter {
1545    previous: Option<Sent>,
1546    system: Option<ColorSystem>,
1547    /// SGR parameters per style id of the screen being painted.
1548    codes: Vec<Option<Sgr>>,
1549    /// The SGR parameters of the style being set, when they are not cached.
1550    scratch: Sgr,
1551    /// True-colour pairs' parameters, per style id.
1552    rgb_codes: RgbCodes,
1553    /// The SGR written to go from one cached style's parameters to
1554    /// another's (by their ids): a frame goes between a few styles many
1555    /// times, so each change is worked out once.
1556    changes: SgrChanges,
1557    /// Painting an inline region below the cursor, with relative moves,
1558    /// rather than the whole screen with absolute ones.
1559    inline: bool,
1560    /// Inline: where the terminal's cursor is, in the region; `None`
1561    /// before the region was made.
1562    cursor: Option<(u16, u16)>,
1563    /// Inline: the region's height.
1564    height: u16,
1565    /// Write every cell inside the damage, changed or not.
1566    force: bool,
1567    /// The cells the last paint wrote, one rectangle per run of a row.
1568    written: Vec<Rect>,
1569    /// The last paint's counts.
1570    stats: PaintStats,
1571    /// How frames become bytes.
1572    strategy: PaintStrategy,
1573    /// Runs of blank cells may be erased (ECH, EL) rather than written.
1574    erase: bool,
1575    /// Rows may be moved with the terminal's scrolling.
1576    scroll: bool,
1577    /// Where nothing may be scrolled through (a picture the terminal does
1578    /// not move with its text).
1579    fixed: Vec<Rect>,
1580    /// The move [`PaintStrategy::Best`] found for this frame.
1581    found: Option<Shift>,
1582}
1583
1584/// The pen's state while a frame is written.
1585#[derive(Default)]
1586struct Pen {
1587    at: Option<(u16, u16)>,
1588    style: Option<StyleId>,
1589    link: Option<String>,
1590    /// Whose SGR parameters are in force (unless `rgb` says).
1591    held: Held,
1592    /// The true-colour pair in force, when the last style set was one:
1593    /// then it, not `codes`, says what the pen holds.
1594    rgb: Option<(Option<Rgb>, Option<Rgb>)>,
1595}
1596
1597/// Whose SGR parameters a [`Pen`] holds: a style's, cached in the
1598/// painter, so moving the pen copies nothing.
1599#[derive(Clone, Copy, Default)]
1600enum Held {
1601    /// Not known: before the first.
1602    #[default]
1603    Unknown,
1604    /// None: after a reset.
1605    Reset,
1606    /// The cached parameters of this style id.
1607    Style(StyleId),
1608}
1609
1610/// No parameters: what a reset leaves.
1611static NO_SGR: Sgr = Sgr {
1612    full: String::new(),
1613    fg: String::new(),
1614    bg: String::new(),
1615    rest: String::new(),
1616};
1617
1618/// The parameters `held` names, from `codes` (the painter's cache).
1619fn held_codes(codes: &[Option<Sgr>], held: Held) -> Option<&Sgr> {
1620    match held {
1621        Held::Unknown => None,
1622        Held::Reset => Some(&NO_SGR),
1623        Held::Style(id) => codes.get(id.index()).and_then(Option::as_ref),
1624    }
1625}
1626
1627/// A style's SGR parameters, split so a change of colour alone can be sent
1628/// as that change: what a game calls a state change, kept small.
1629#[derive(Clone, Debug, Default, PartialEq, Eq)]
1630struct Sgr {
1631    /// All of them, in the order the style writes them.
1632    full: String,
1633    /// The foreground colour's parameters (`31`, `38;2;r;g;b`), if any.
1634    fg: String,
1635    /// The background colour's.
1636    bg: String,
1637    /// Everything else (attributes, underline colour), in order.
1638    rest: String,
1639}
1640
1641impl Sgr {
1642    fn parse(codes: &str) -> Sgr {
1643        let mut sgr = Sgr {
1644            full: codes.to_string(),
1645            ..Sgr::default()
1646        };
1647        let tokens: Vec<&str> = codes.split(';').filter(|t| !t.is_empty()).collect();
1648        let mut i = 0;
1649        while i < tokens.len() {
1650            let token = tokens[i];
1651            // An extended colour takes its sub-parameters with it.
1652            let take = match (token, tokens.get(i + 1).copied()) {
1653                ("38" | "48" | "58", Some("5")) => 3,
1654                ("38" | "48" | "58", Some("2")) => 5,
1655                _ => 1,
1656            };
1657            let end = (i + take).min(tokens.len());
1658            let group = tokens[i..end].join(";");
1659            let n: u16 = token.parse().unwrap_or(0);
1660            let (slot, colour) = match n {
1661                30..=39 | 90..=97 => (&mut sgr.fg, true),
1662                40..=49 | 100..=107 => (&mut sgr.bg, true),
1663                _ => (&mut sgr.rest, false),
1664            };
1665            if colour {
1666                // A colour set twice: the last wins, as on a terminal.
1667                slot.clear();
1668            }
1669            if !slot.is_empty() {
1670                slot.push(';');
1671            }
1672            slot.push_str(&group);
1673            i = end;
1674        }
1675        sgr
1676    }
1677
1678    /// Set from a true-colour pair, as `Style::ansi_codes` writes it.
1679    fn set_rgb(&mut self, fg: Option<Rgb>, bg: Option<Rgb>) {
1680        self.fg.clear();
1681        self.bg.clear();
1682        self.rest.clear();
1683        self.full.clear();
1684        if let Some(fg) = fg {
1685            rgb_code(&mut self.fg, "38;2;", fg);
1686        }
1687        if let Some(bg) = bg {
1688            rgb_code(&mut self.bg, "48;2;", bg);
1689        }
1690        self.full.push_str(&self.fg);
1691        if !self.fg.is_empty() && !self.bg.is_empty() {
1692            self.full.push(';');
1693        }
1694        self.full.push_str(&self.bg);
1695    }
1696
1697    /// Write what changes the terminal's pen from `from` (unknown when
1698    /// `None`) to this: nothing, the colours that changed, or a reset and
1699    /// everything, whichever is shortest.
1700    fn write_from(&self, from: Option<&Sgr>, out: &mut String) {
1701        let full_len = 4 + if self.full.is_empty() {
1702            0
1703        } else {
1704            1 + self.full.len()
1705        };
1706        if let Some(from) = from {
1707            if from.full == self.full {
1708                return;
1709            }
1710            if from.rest == self.rest {
1711                let part = |now: &str, was: &str, default: usize| match (now == was, now.is_empty())
1712                {
1713                    (true, _) => 0,
1714                    (false, true) => default,
1715                    (false, false) => now.len(),
1716                };
1717                let fg = part(&self.fg, &from.fg, 2);
1718                let bg = part(&self.bg, &from.bg, 2);
1719                let delta = 3 + fg + bg + usize::from(fg > 0 && bg > 0);
1720                if delta < full_len {
1721                    out.push_str("\x1b[");
1722                    if fg > 0 {
1723                        out.push_str(if self.fg.is_empty() { "39" } else { &self.fg });
1724                    }
1725                    if fg > 0 && bg > 0 {
1726                        out.push(';');
1727                    }
1728                    if bg > 0 {
1729                        out.push_str(if self.bg.is_empty() { "49" } else { &self.bg });
1730                    }
1731                    out.push('m');
1732                    return;
1733                }
1734            }
1735        }
1736        out.push_str("\x1b[0");
1737        if !self.full.is_empty() {
1738            out.push(';');
1739            out.push_str(&self.full);
1740        }
1741        out.push('m');
1742    }
1743}
1744
1745impl Painter {
1746    /// A painter for `system`, under [`PaintStrategy::Cells`] until
1747    /// [`set_strategy`](Self::set_strategy): it moves no rows until the
1748    /// caller, who knows where pictures sit and whether tmux is in the way,
1749    /// asks it to. An intuiTUIve app paints with `best`.
1750    pub fn new(system: Option<ColorSystem>) -> Painter {
1751        Painter {
1752            previous: None,
1753            system,
1754            codes: Vec::new(),
1755            scratch: Sgr::default(),
1756            rgb_codes: RgbCodes::default(),
1757            changes: SgrChanges::default(),
1758            inline: false,
1759            cursor: None,
1760            height: 0,
1761            force: false,
1762            written: Vec::new(),
1763            stats: PaintStats::default(),
1764            // A bare painter knows nothing of pictures or tmux (an app
1765            // tells it, through `set_fixed` and `set_scroll`), so it moves
1766            // no rows unless asked.
1767            strategy: PaintStrategy::Cells,
1768            erase: true,
1769            // Windows consoles have been seen to scroll the wrong rows.
1770            scroll: !cfg!(windows),
1771            fixed: Vec::new(),
1772            found: None,
1773        }
1774    }
1775
1776    /// Whether rows may be moved with the terminal's own scrolling (SU, SD
1777    /// and DECSTBM) under [`PaintStrategy::Scroll`] and
1778    /// [`PaintStrategy::Best`]: on by default but on Windows. A terminal
1779    /// multiplexer that redraws scroll regions itself gains little.
1780    pub fn set_scroll(&mut self, on: bool) {
1781        self.scroll = on;
1782    }
1783
1784    /// Rectangles no scroll may move through (Sixel or iTerm2 pictures,
1785    /// which a terminal does not move with the rows); until the next call.
1786    pub fn set_fixed(&mut self, rects: &[Rect]) {
1787        self.fixed.clear();
1788        self.fixed.extend_from_slice(rects);
1789    }
1790
1791    /// Whether runs of blank cells may be erased with ECH or EL rather
1792    /// than written as spaces (on by default; only blanks with the
1793    /// terminal's own background are, so a terminal without
1794    /// background-colour erase shows the same).
1795    pub fn set_erase(&mut self, on: bool) {
1796        self.erase = on;
1797    }
1798
1799    /// Paint with `strategy` from now on.
1800    pub fn set_strategy(&mut self, strategy: PaintStrategy) {
1801        self.strategy = strategy;
1802    }
1803
1804    /// The strategy this painter paints with.
1805    pub fn strategy(&self) -> PaintStrategy {
1806        self.strategy
1807    }
1808
1809    /// What the last [`paint`](Self::paint) or [`repaint`](Self::repaint)
1810    /// did: cells compared and written, and bytes by kind.
1811    pub fn stats(&self) -> PaintStats {
1812        self.stats
1813    }
1814
1815    /// Paint an inline region that starts on the cursor's row, moving the
1816    /// cursor relative to where it is, so the rows above (the shell's
1817    /// scrollback) are left alone.
1818    #[must_use]
1819    pub fn inline(mut self) -> Painter {
1820        self.inline = true;
1821        self
1822    }
1823
1824    pub fn set_color_system(&mut self, system: Option<ColorSystem>) {
1825        if system != self.system {
1826            self.system = system;
1827            self.codes.clear();
1828            self.changes.clear();
1829        }
1830    }
1831
1832    /// Move the cursor to `x`, `y` of the screen.
1833    pub fn move_to(&mut self, x: u16, y: u16) -> String {
1834        let out = self.goto(self.cursor, x, y);
1835        if self.inline {
1836            self.cursor = Some((x, y));
1837        }
1838        out
1839    }
1840
1841    /// What to write when the app ends: inline, the cursor goes to the
1842    /// start of the line below the region, so what follows prints under
1843    /// the app's last frame.
1844    pub fn finish(&mut self) -> String {
1845        if !self.inline || self.cursor.is_none() {
1846            return String::new();
1847        }
1848        let out = self.move_to(0, self.height.saturating_sub(1));
1849        self.cursor = None;
1850        format!("{out}\x1b[0m\r\n")
1851    }
1852
1853    fn goto(&self, from: Option<(u16, u16)>, x: u16, y: u16) -> String {
1854        let mut out = String::new();
1855        self.goto_into(from, x, y, u16::MAX, &mut out);
1856        out
1857    }
1858
1859    /// Move the cursor from `from` (unknown when `None`) to `x`, `y`, the
1860    /// cheapest way: an absolute move (CUP), or a relative one (up, down,
1861    /// left, right, a carriage return, `\r\n`) when it is shorter.
1862    /// `width` is the screen's: a cursor past its last column is waiting
1863    /// to wrap, so only a carriage return fixes its column.
1864    fn goto_into(&self, from: Option<(u16, u16)>, x: u16, y: u16, width: u16, out: &mut String) {
1865        if self.inline {
1866            let (_, from_y) = from.unwrap_or((0, 0));
1867            if y < from_y {
1868                csi(out, from_y - y, 'A');
1869            } else if y > from_y {
1870                csi(out, y - from_y, 'B');
1871            }
1872            // A carriage return also clears a pending wrap after the last
1873            // column, so the column is always counted from the left edge.
1874            out.push('\r');
1875            if x > 0 {
1876                csi(out, x, 'C');
1877            }
1878            return;
1879        }
1880        let cup = if x == 0 && y == 0 {
1881            3
1882        } else {
1883            3 + digits(y + 1) + if x > 0 { 1 + digits(x + 1) } else { 0 }
1884        };
1885        let Some((fx, fy)) = from else {
1886            return push_cup(out, x, y);
1887        };
1888        let step = |n: u16| if n == 1 { 3 } else { 3 + digits(n) };
1889        let vertical = match y.cmp(&fy) {
1890            std::cmp::Ordering::Equal => 0,
1891            std::cmp::Ordering::Greater => step(y - fy),
1892            std::cmp::Ordering::Less => step(fy - y),
1893        };
1894        let wrapping = fx >= width;
1895        // Along the row, from where the cursor is (not while it waits to
1896        // wrap), or from the left edge after a carriage return.
1897        let along = if wrapping || x == fx {
1898            usize::MAX
1899        } else if x > fx {
1900            step(x - fx)
1901        } else {
1902            step(fx - x)
1903        };
1904        let along = if x == fx && !wrapping { 0 } else { along };
1905        let from_edge = 1 + if x > 0 { step(x) } else { 0 };
1906        let crlf = x == 0 && y == fy + 1;
1907        let relative = vertical.saturating_add(along.min(from_edge));
1908        if crlf && 2 < cup && 2 <= relative {
1909            out.push_str("\r\n");
1910            return;
1911        }
1912        if relative >= cup {
1913            return push_cup(out, x, y);
1914        }
1915        if along > from_edge {
1916            out.push('\r');
1917        }
1918        match y.cmp(&fy) {
1919            std::cmp::Ordering::Greater => csi(out, y - fy, 'B'),
1920            std::cmp::Ordering::Less => csi(out, fy - y, 'A'),
1921            std::cmp::Ordering::Equal => {}
1922        }
1923        if along <= from_edge {
1924            if x > fx {
1925                csi(out, x - fx, 'C');
1926            } else if x < fx {
1927                csi(out, fx - x, 'D');
1928            }
1929        } else if x > 0 {
1930            csi(out, x, 'C');
1931        }
1932    }
1933
1934    /// Inline: go back to the region's first row (if it was made), and
1935    /// make room for `height` rows below the cursor, scrolling the
1936    /// terminal if the region would pass its bottom, and clear them.
1937    fn reserve(&mut self, height: u16) -> String {
1938        let mut out = String::from("\x1b[0m");
1939        if let Some((_, y)) = self.cursor
1940            && y > 0
1941        {
1942            out.push_str(&format!("\x1b[{y}A"));
1943        }
1944        out.push('\r');
1945        if height > 1 {
1946            out.push_str(&"\n".repeat(height as usize - 1));
1947            out.push_str(&format!("\x1b[{}A", height - 1));
1948        }
1949        out.push_str("\x1b[J");
1950        self.cursor = Some((0, 0));
1951        self.height = height;
1952        out
1953    }
1954
1955    /// Forget what was sent: the next paint writes everything.
1956    pub fn invalidate(&mut self) {
1957        self.previous = None;
1958    }
1959
1960    /// The screen this painter paints had its styles renumbered
1961    /// ([`Screen::compact_styles`]): follow the new ids, so what was sent
1962    /// still compares with it. A cell sent in a style that was dropped
1963    /// reads as changed.
1964    pub fn remap_styles(&mut self, remap: &StyleRemap) {
1965        if let Some(previous) = &mut self.previous {
1966            for cell in &mut previous.cells {
1967                cell.style = remap.get_or_gone(cell.style);
1968            }
1969        }
1970        let mut codes = Vec::with_capacity(self.codes.len());
1971        for (old, code) in self.codes.drain(..).enumerate() {
1972            let Some(code) = code else { continue };
1973            if let Some(new) = remap.get(StyleId::from_index(old)) {
1974                if codes.len() <= new.index() {
1975                    codes.resize(new.index() + 1, None);
1976                }
1977                codes[new.index()] = Some(code);
1978            }
1979        }
1980        self.codes = codes;
1981        self.rgb_codes.clear();
1982        self.changes.clear();
1983    }
1984
1985    /// Drop the styles `screen`'s cells no longer use, when its table has
1986    /// piled up ([`Screen::styles_piled_up`]), keeping this painter in
1987    /// step: call it after each [`paint`](Self::paint), and a screen whose
1988    /// cells change colour every frame keeps only the styles of a frame or
1989    /// two. Whether it compacted. What is painted next is the same either
1990    /// way.
1991    ///
1992    /// ```
1993    /// use rich::ColorSystem;
1994    /// use rich_ext::surface::{Painter, Screen};
1995    ///
1996    /// let mut screen = Screen::new(16, 4);
1997    /// let mut painter = Painter::new(Some(ColorSystem::Truecolor));
1998    /// for frame in 0..100u8 {
1999    ///     for y in 0..4 {
2000    ///         for x in 0..16 {
2001    ///             let c = [frame, x as u8 * 16, y as u8 * 64];
2002    ///             screen.put_rgb(x, y, '▀', Some(c), Some([0, 0, frame]));
2003    ///         }
2004    ///     }
2005    ///     painter.paint(&screen, &[screen.area()]);
2006    ///     painter.compact(&mut screen);
2007    /// }
2008    /// // A frame uses 64 styles; a hundred frames made 6,400.
2009    /// assert!(screen.style_count() < 2_000);
2010    /// ```
2011    pub fn compact(&mut self, screen: &mut Screen) -> bool {
2012        if !screen.styles_piled_up() {
2013            return false;
2014        }
2015        let remap = screen.compact_styles();
2016        self.remap_styles(&remap);
2017        true
2018    }
2019
2020    /// What to write so the terminal shows `screen`, given that only the
2021    /// `damage` rectangles can have changed since the last paint (ignored
2022    /// on the first paint, or after a resize or [`invalidate`](Self::invalidate)).
2023    pub fn paint(&mut self, screen: &Screen, damage: &[Rect]) -> String {
2024        let mut out = String::new();
2025        self.paint_into(screen, damage, &mut out);
2026        out
2027    }
2028
2029    /// [`paint`](Self::paint), appending to `out`: one buffer for a whole
2030    /// frame.
2031    pub fn paint_into(&mut self, screen: &Screen, damage: &[Rect], out: &mut String) {
2032        self.written.clear();
2033        self.stats = PaintStats::default();
2034        let base = out.len();
2035        let first = !matches!(&self.previous, Some(p) if p.width == screen.width && p.height == screen.height);
2036        let mut rects: Vec<Rect> = if first {
2037            if self.inline {
2038                out.push_str(&self.reserve(screen.height));
2039            } else {
2040                out.push_str("\x1b[0m\x1b[H\x1b[2J");
2041            }
2042            self.previous = None;
2043            self.codes.clear();
2044            self.rgb_codes.clear();
2045            self.changes.clear();
2046            vec![screen.area()]
2047        } else {
2048            // A column either side too: writing a rectangle can turn the
2049            // half of a wide grapheme it cut, just outside it, into a space.
2050            // (Not for a forced repaint, which writes every cell it is given.)
2051            let wide = u16::from(!self.force);
2052            damage
2053                .iter()
2054                .map(|r| {
2055                    let x = r.x.saturating_sub(wide);
2056                    let right = r.right().saturating_add(wide);
2057                    Rect::new(x, r.y, right - x, r.height).intersection(screen.area())
2058                })
2059                .filter(|r| !r.is_empty())
2060                .collect()
2061        };
2062        let mut pen = Pen {
2063            at: self.cursor,
2064            ..Pen::default()
2065        };
2066        let mut strategy = match self.strategy {
2067            _ if first || self.force => PaintStrategy::Cells,
2068            PaintStrategy::Best => self.choose(screen, &rects),
2069            other => other,
2070        };
2071        if strategy == PaintStrategy::Scroll {
2072            let found = match self.found.take() {
2073                Some(shift) => Some(shift),
2074                None => self.find_shift(screen, &rects),
2075            };
2076            match found {
2077                Some(shift) => {
2078                    let before = out.len();
2079                    self.scroll_by(shift, screen, &mut pen, out);
2080                    self.stats.other_bytes += out.len() - before;
2081                    rects.push(Rect::new(
2082                        0,
2083                        shift.top,
2084                        screen.width,
2085                        shift.bottom - shift.top,
2086                    ));
2087                }
2088                None => strategy = PaintStrategy::Cells,
2089            }
2090        }
2091        let forced = self.force;
2092        if strategy == PaintStrategy::Full {
2093            rects = vec![screen.area()];
2094            self.force = true;
2095        }
2096        for (y, spans) in row_spans(&rects, screen.height) {
2097            if strategy == PaintStrategy::Rows {
2098                self.paint_row(screen, y, &mut pen, out);
2099                continue;
2100            }
2101            for (start, end) in spans {
2102                // A span the same as what was sent is passed over whole: a
2103                // row of cells compares as one slice.
2104                if !self.force
2105                    && let Some(previous) = &self.previous
2106                {
2107                    let width = screen.width as usize;
2108                    let row = y as usize * width;
2109                    let span = row + start as usize..row + end as usize;
2110                    if previous.cells[span.clone()] == screen.cells[span] {
2111                        self.stats.compared += usize::from(end - start);
2112                        continue;
2113                    }
2114                }
2115                self.paint_span(screen, y, start, end, &mut pen, out);
2116            }
2117        }
2118        self.force = forced;
2119        let before = out.len();
2120        if pen.link.is_some() {
2121            out.push_str("\x1b]8;;\x1b\\");
2122        }
2123        if pen.rgb.is_some()
2124            || held_codes(&self.codes, pen.held).is_some_and(|c| !c.full.is_empty())
2125        {
2126            out.push_str("\x1b[0m");
2127        }
2128        self.stats.style_bytes += out.len() - before;
2129        let stats = &mut self.stats;
2130        stats.strategy = strategy;
2131        stats.elided = stats.compared.saturating_sub(stats.written);
2132        stats.other_bytes =
2133            out.len() - base - stats.move_bytes - stats.style_bytes - stats.text_bytes;
2134        if self.inline {
2135            self.cursor = pen.at.or(self.cursor);
2136        }
2137        // Bring the copy of what was sent up to date.
2138        match &mut self.previous {
2139            None => {
2140                self.previous = Some(Sent {
2141                    width: screen.width,
2142                    height: screen.height,
2143                    cells: screen.cells.clone(),
2144                })
2145            }
2146            Some(previous) => {
2147                let width = screen.width as usize;
2148                for rect in &rects {
2149                    for y in rect.y..rect.bottom() {
2150                        let row = y as usize * width;
2151                        let span = row + rect.x as usize..row + rect.right() as usize;
2152                        previous.cells[span.clone()].copy_from_slice(&screen.cells[span]);
2153                    }
2154                }
2155            }
2156        }
2157    }
2158
2159    /// [`PaintStrategy::Best`]'s pick for this frame.
2160    ///
2161    /// Choosing must cost less than it saves, so it encodes nothing twice:
2162    /// a frame where rows moved together scrolls them (rows compare as
2163    /// slices of plain cells, so finding the move is cheap); any other is
2164    /// painted cell by cell. The benchmark found `rows` never more than a
2165    /// few percent shorter once the cell encoder writes through short
2166    /// gaps, so it is not tried; nor is `full` for a frame where nearly
2167    /// every cell changed, as painting cells by cell skips the unchanged
2168    /// ones with fewer bytes and, on the benchmark's machines, no more
2169    /// time (`full` was up to 17% slower on CI's).
2170    fn choose(&mut self, screen: &Screen, rects: &[Rect]) -> PaintStrategy {
2171        self.found = self.find_shift(screen, rects);
2172        match self.found {
2173            Some(_) => PaintStrategy::Scroll,
2174            None => PaintStrategy::Cells,
2175        }
2176    }
2177
2178    /// [`PaintStrategy::Rows`] on row `y`: when it differs from what was
2179    /// sent, every cell from its first change to its last.
2180    fn paint_row(&mut self, screen: &Screen, y: u16, pen: &mut Pen, out: &mut String) {
2181        let Some(previous) = &self.previous else {
2182            return;
2183        };
2184        let (sent, now) = (previous.row(y), screen.row(y));
2185        let width = screen.width as usize;
2186        self.stats.compared += width;
2187        let Some(first) = (0..width).find(|&x| sent[x] != now[x]) else {
2188            return;
2189        };
2190        let last = (0..width).rfind(|&x| sent[x] != now[x]).unwrap_or(first);
2191        // A wide grapheme's second column is written with its first.
2192        let end = (last + 1 + usize::from(now.get(last).is_some_and(|c| c.width >= 2))).min(width);
2193        let forced = self.force;
2194        self.force = true;
2195        let compared = self.stats.compared;
2196        self.paint_span(screen, y, first as u16, end as u16, pen, out);
2197        // Counted once, above.
2198        self.stats.compared = compared;
2199        self.force = forced;
2200    }
2201
2202    /// The best run of rows that moved together since they were sent, if
2203    /// scrolling is allowed and one is worth it: the shift that puts the
2204    /// most changed rows right, looking first at the scrolls the screen
2205    /// was told of.
2206    fn find_shift(&mut self, screen: &Screen, rects: &[Rect]) -> Option<Shift> {
2207        if !self.scroll || self.inline {
2208            return None;
2209        }
2210        let previous = self.previous.as_ref()?;
2211        let (top, bottom) = rects
2212            .iter()
2213            .fold((u16::MAX, 0), |(t, b), r| (t.min(r.y), b.max(r.bottom())));
2214        if bottom <= top || bottom - top < 3 {
2215            return None;
2216        }
2217        let height = screen.height;
2218        // Rows compare as slices of plain cells: a pair that differs stops
2219        // at its first cell, so looking for where a row went is cheap.
2220        let same = |y: u16, source: u16| screen.row(y) == previous.row(source);
2221        // Candidate moves: those the screen was told of, then those the
2222        // first changed rows vote for (where each is found among the rows
2223        // sent near it).
2224        let mut votes: Vec<(i32, u16)> = Vec::new();
2225        let mut vote = |by: i32, n: u16| match votes.iter_mut().find(|(b, _)| *b == by) {
2226            Some((_, count)) => *count += n,
2227            None => votes.push((by, n)),
2228        };
2229        for &(_, rows) in &screen.scrolled {
2230            vote(i32::from(rows), height);
2231        }
2232        // Only rows inside the damage can have changed.
2233        let mut damaged = vec![false; height as usize];
2234        for rect in rects {
2235            for y in rect.y..rect.bottom().min(height) {
2236                damaged[y as usize] = true;
2237            }
2238        }
2239        let mut changed = 0;
2240        let mut found: Vec<u16> = Vec::new();
2241        for y in (top..bottom).filter(|&y| damaged[y as usize]) {
2242            if same(y, y) {
2243                continue;
2244            }
2245            changed += 1;
2246            found.clear();
2247            'look: for d in 1..=SCROLL_WINDOW.min(height) {
2248                for source in [y.checked_add(d), y.checked_sub(d)].into_iter().flatten() {
2249                    if source < height && same(y, source) {
2250                        found.push(source);
2251                        // A row found in many places (a blank one) says
2252                        // nothing of a move.
2253                        if found.len() > 8 {
2254                            break 'look;
2255                        }
2256                    }
2257                }
2258            }
2259            if found.len() <= 8 {
2260                for &source in &found {
2261                    vote(i32::from(source) - i32::from(y), 1);
2262                }
2263            }
2264            if changed >= 6 {
2265                break;
2266            }
2267        }
2268        if changed < 3 || votes.is_empty() {
2269            return None;
2270        }
2271        let mut best: Option<Shift> = None;
2272        let consider = |by: i32, best: &mut Option<Shift>| {
2273            // The longest run of rows y showing what row y + by showed.
2274            let mut run_start = None;
2275            let mut y = 0i32;
2276            while y <= height as i32 {
2277                let source = y + by;
2278                let matches = y < height as i32
2279                    && (0..height as i32).contains(&source)
2280                    && same(y as u16, source as u16);
2281                match (matches, run_start) {
2282                    (true, None) => run_start = Some(y),
2283                    (false, Some(start)) => {
2284                        run_start = None;
2285                        let gain = (start..y).filter(|&r| !same(r as u16, r as u16)).count() as u16;
2286                        let (top, bottom) = if by > 0 {
2287                            (start as u16, (y + by) as u16)
2288                        } else {
2289                            ((start + by) as u16, y as u16)
2290                        };
2291                        if best.is_none_or(|b| gain > b.gain) {
2292                            *best = Some(Shift {
2293                                top,
2294                                bottom,
2295                                by,
2296                                gain,
2297                            });
2298                        }
2299                    }
2300                    _ => {}
2301                }
2302                y += 1;
2303            }
2304        };
2305        votes.sort_unstable_by_key(|&(by, n)| (std::cmp::Reverse(n), by.abs()));
2306        for &(by, _) in votes.iter().take(3) {
2307            consider(by, &mut best);
2308        }
2309        let shift = best?;
2310        // Worth a scroll: it puts at least two rows right, and more than it
2311        // opens.
2312        if shift.gain < 2 || i32::from(shift.gain) <= shift.by.abs() {
2313            return None;
2314        }
2315        // No picture may sit in the rows that move.
2316        let region = Rect::new(0, shift.top, screen.width, shift.bottom - shift.top);
2317        if self.fixed.iter().any(|r| r.intersects(region)) {
2318            return None;
2319        }
2320        Some(shift)
2321    }
2322
2323    /// Move rows with the terminal's scrolling: the whole screen with SU or
2324    /// SD, else inside DECSTBM margins (which home the cursor), the pen's
2325    /// colours reset first so the rows opened are blank. The copy of what
2326    /// was sent moves with them.
2327    fn scroll_by(&mut self, shift: Shift, screen: &Screen, pen: &mut Pen, out: &mut String) {
2328        out.push_str("\x1b[0m");
2329        pen.held = Held::Reset;
2330        pen.rgb = None;
2331        pen.style = None;
2332        let whole = shift.top == 0 && shift.bottom == screen.height;
2333        if !whole {
2334            out.push_str("\x1b[");
2335            push_u16(out, shift.top + 1);
2336            out.push(';');
2337            push_u16(out, shift.bottom);
2338            out.push('r');
2339        }
2340        let n = shift.by.unsigned_abs() as u16;
2341        csi(out, n, if shift.by > 0 { 'S' } else { 'T' });
2342        if !whole {
2343            out.push_str("\x1b[r");
2344        }
2345        // Where the cursor is now is the terminal's business.
2346        pen.at = None;
2347        if let Some(previous) = &mut self.previous {
2348            previous.shift(shift.top, shift.bottom, shift.by);
2349        }
2350    }
2351
2352    /// The cells the last [`paint`](Self::paint) or [`repaint`](Self::repaint)
2353    /// wrote, one rectangle per run of a row: what a graphic drawn over them
2354    /// lost.
2355    pub fn written(&self) -> &[Rect] {
2356        &self.written
2357    }
2358
2359    /// [`paint`](Self::paint), writing every cell of `rects` whether it
2360    /// changed or not: for cells something else drew over (a graphic that
2361    /// went away).
2362    pub fn repaint(&mut self, screen: &Screen, rects: &[Rect]) -> String {
2363        self.force = true;
2364        let out = self.paint(screen, rects);
2365        self.force = false;
2366        out
2367    }
2368
2369    fn paint_span(
2370        &mut self,
2371        screen: &Screen,
2372        y: u16,
2373        start: u16,
2374        end: u16,
2375        pen: &mut Pen,
2376        out: &mut String,
2377    ) {
2378        let mut x = start;
2379        // A span that starts on a wide grapheme's second column starts at
2380        // its first.
2381        if x > 0 && screen.cell(x, y).is_continuation() {
2382            x -= 1;
2383        }
2384        while x < end {
2385            // Past the cells the same as what was sent, a run at a time.
2386            if !self.force
2387                && let Some(previous) = &self.previous
2388            {
2389                let row = y as usize * screen.width as usize;
2390                let (from, to) = (row + x as usize, row + end as usize);
2391                let same = previous.cells[from..to]
2392                    .iter()
2393                    .zip(&screen.cells[from..to])
2394                    .take_while(|(sent, now)| sent == now)
2395                    .count();
2396                self.stats.compared += same;
2397                x += same as u16;
2398                if x >= end {
2399                    break;
2400                }
2401            }
2402            let mut x0 = x;
2403            if screen.cell(x0, y).is_continuation() && x0 > 0 {
2404                x0 -= 1;
2405            }
2406            // A run of blanks on the terminal's own background: erased
2407            // (EL to the end of the row, ECH within it) when that is
2408            // shorter than writing it.
2409            if self.erase && !self.inline && self.plain_blank(screen, x0, y) {
2410                // Blanks the pen would skip on its way here join the run,
2411                // so the erase starts where the pen is.
2412                if let Some((fx, fy)) = pen.at
2413                    && fy == y
2414                    && fx < x0
2415                    && (fx..x0).all(|gx| self.plain_blank(screen, gx, y))
2416                {
2417                    self.stats.compared += usize::from(x0 - fx);
2418                    x = fx;
2419                    x0 = fx;
2420                }
2421                let mut run = 1;
2422                while x0 + run < end && self.plain_blank(screen, x0 + run, y) {
2423                    run += 1;
2424                }
2425                let to_edge = x0 + run == screen.width;
2426                let erase = if to_edge { 3 } else { 3 + digits(run) };
2427                if usize::from(run) > erase {
2428                    self.move_pen(screen, x0, y, pen, out);
2429                    let before = out.len();
2430                    self.set_pen(screen, StyleId::NONE, pen, out);
2431                    self.stats.style_bytes += out.len() - before;
2432                    if to_edge {
2433                        out.push_str("\x1b[K");
2434                    } else {
2435                        csi(out, run, 'X');
2436                    }
2437                    self.stats.compared += usize::from(x0 + run - x);
2438                    self.stats.written += usize::from(run);
2439                    self.note_written(x0, y, x0 + run);
2440                    x = x0 + run;
2441                    continue;
2442                }
2443            }
2444            self.move_pen(screen, x0, y, pen, out);
2445            let cell = screen.cell(x0, y);
2446            let before = out.len();
2447            self.set_pen(screen, cell.style, pen, out);
2448            self.stats.style_bytes += out.len() - before;
2449            // An emoji with a selector or joiners is a width terminals
2450            // disagree on: where the cursor ended up is not known, so the
2451            // next write moves there.
2452            let several = match cell.single_char() {
2453                Some(c) => {
2454                    out.push(c);
2455                    self.stats.text_bytes += c.len_utf8();
2456                    c == '\u{200d}' || c == '\u{fe0f}'
2457                }
2458                None => {
2459                    let text = cell.text();
2460                    out.push_str(text);
2461                    self.stats.text_bytes += text.len();
2462                    cell.len == LONG || text.contains(['\u{200d}', '\u{fe0f}'])
2463                }
2464            };
2465            let next = x0 + cell.width.max(1) as u16;
2466            let columns = usize::from(next - x0);
2467            self.stats.compared += usize::from(next - x);
2468            self.stats.written += columns;
2469            self.note_written(x0, y, next);
2470            pen.at = (!several).then_some((next, y));
2471            x = next;
2472        }
2473    }
2474
2475    /// Note cells `x`..`next` of row `y` as written this paint.
2476    fn note_written(&mut self, x: u16, y: u16, next: u16) {
2477        match self.written.last_mut() {
2478            Some(run) if run.y == y && run.right() == x => run.width = next - run.x,
2479            _ => self.written.push(Rect::new(x, y, next - x, 1)),
2480        }
2481    }
2482
2483    /// Move the pen to `x`, `y`: through the cells between, when they are
2484    /// a few unchanged ASCII characters in the pen's style (shorter than
2485    /// any move), else the cheapest move.
2486    fn move_pen(&mut self, screen: &Screen, x: u16, y: u16, pen: &mut Pen, out: &mut String) {
2487        if pen.at == Some((x, y)) {
2488            return;
2489        }
2490        let before = out.len();
2491        self.goto_into(pen.at, x, y, screen.width, out);
2492        let jump = out.len() - before;
2493        // Writing the unchanged cells between again (what the terminal
2494        // shows already) can be shorter than the move, as Mosh does: a few
2495        // narrow cells, counting the style changes they need.
2496        if let Some((fx, fy)) = pen.at.filter(|&(fx, fy)| {
2497            !self.inline && fy == y && fx < x && x - fx <= 8 && jump > usize::from(x - fx)
2498        }) {
2499            let mut cost = 0;
2500            let mut style = pen.style;
2501            for gx in fx..x {
2502                let cell = screen.cell(gx, fy);
2503                if cell.width != 1 || cell.len == LONG || cell.len == 0 {
2504                    cost = usize::MAX;
2505                    break;
2506                }
2507                cost += cell.len as usize;
2508                if style != Some(cell.style) {
2509                    style = Some(cell.style);
2510                    // A change of style: at most a reset and its parameters.
2511                    cost += match screen.styles.rgb(cell.style) {
2512                        Some(_) => 40,
2513                        None if cell.style == StyleId::NONE => 4,
2514                        None => 4 + self.sgr(screen, cell.style).full.len(),
2515                    };
2516                }
2517                if cost >= jump {
2518                    break;
2519                }
2520            }
2521            if style != pen.style && cost < jump {
2522                // Coming back to the pen's style costs too.
2523                cost += 8;
2524            }
2525            if cost < jump {
2526                out.truncate(before);
2527                for gx in fx..x {
2528                    let cell = *screen.cell(gx, y);
2529                    self.set_pen(screen, cell.style, pen, out);
2530                    out.push_str(cell.text());
2531                }
2532            }
2533        }
2534        self.stats.move_bytes += out.len() - before;
2535        pen.at = Some((x, y));
2536    }
2537
2538    /// Whether the cell at `x`, `y` is a space with no style the terminal
2539    /// shows on a blank: what erasing leaves.
2540    fn plain_blank(&mut self, screen: &Screen, x: u16, y: u16) -> bool {
2541        let cell = screen.cell(x, y);
2542        if cell.len != 1 || cell.bytes[0] != b' ' || cell.width != 1 {
2543            return false;
2544        }
2545        if cell.style == StyleId::NONE {
2546            return true;
2547        }
2548        let Some(style) = screen.style(cell.style) else {
2549            return true;
2550        };
2551        // Colours and attributes would show on the blank; a link would not
2552        // be opened by an erase.
2553        style.link().is_none() && self.sgr(screen, cell.style).full.is_empty()
2554    }
2555
2556    /// The SGR parameters of `style`, cached by id.
2557    fn sgr(&mut self, screen: &Screen, style: StyleId) -> &Sgr {
2558        if self.codes.len() <= style.index() {
2559            self.codes.resize(style.index() + 1, None);
2560        }
2561        let system = self.system;
2562        self.codes[style.index()].get_or_insert_with(|| match (screen.style(style), system) {
2563            (Some(s), Some(system)) => Sgr::parse(&s.ansi_codes(system)),
2564            _ => Sgr::default(),
2565        })
2566    }
2567
2568    fn set_pen(&mut self, screen: &Screen, style: StyleId, pen: &mut Pen, out: &mut String) {
2569        if pen.style == Some(style) {
2570            return;
2571        }
2572        // A colour pair (no link) in true colour while no link is open:
2573        // straight to the colours.
2574        if pen.link.is_none()
2575            && let (Some(pair), Some(ColorSystem::Truecolor), Some(old)) =
2576                (screen.styles.rgb(style), self.system, pen.rgb)
2577        {
2578            if old != pair {
2579                rgb_delta(old, pair, self.rgb_codes.get(style, pair), out);
2580            }
2581            pen.rgb = Some(pair);
2582            pen.style = Some(style);
2583            return;
2584        }
2585        let resolved = screen.style(style);
2586        // A URL holding a control would end the OSC early and drive the
2587        // terminal with the rest: such a link is dropped.
2588        let link = resolved
2589            .and_then(|s| s.link())
2590            .filter(|url| !url.chars().any(|c| super::control_picture(c).is_some()));
2591        if link != pen.link.as_deref() {
2592            match link {
2593                Some(url) => {
2594                    out.push_str("\x1b]8;;");
2595                    out.push_str(url);
2596                    out.push_str("\x1b\\");
2597                }
2598                None => out.push_str("\x1b]8;;\x1b\\"),
2599            }
2600            pen.link = link.map(str::to_string);
2601        }
2602        if let (Some(pair), Some(ColorSystem::Truecolor)) = (screen.styles.rgb(style), self.system)
2603        {
2604            // A colour pair in true colour: written straight from the
2605            // colours, as `Style::ansi_codes` would, with nothing cached,
2606            // and from the last pair only the colour that changed.
2607            match pen.rgb {
2608                Some(old) if old == pair => {}
2609                Some(old) => rgb_delta(old, pair, self.rgb_codes.get(style, pair), out),
2610                None => {
2611                    self.scratch.set_rgb(pair.0, pair.1);
2612                    self.scratch
2613                        .write_from(held_codes(&self.codes, pen.held), out);
2614                }
2615            }
2616            pen.rgb = Some(pair);
2617            pen.style = Some(style);
2618            return;
2619        }
2620        if let (None, Held::Style(held)) = (pen.rgb, pen.held) {
2621            // From one cached style to another: worked out once.
2622            if let Some(change) = self.changes.get(held, style) {
2623                out.push_str(change);
2624                pen.held = Held::Style(style);
2625                pen.style = Some(style);
2626                return;
2627            }
2628            self.sgr(screen, style);
2629            let codes = self.codes[style.index()].as_ref().expect("cached above");
2630            let before = out.len();
2631            codes.write_from(held_codes(&self.codes, pen.held), out);
2632            self.changes.put(held, style, &out[before..]);
2633            pen.held = Held::Style(style);
2634            pen.style = Some(style);
2635            return;
2636        }
2637        self.sgr(screen, style);
2638        let codes = self.codes[style.index()].as_ref().expect("cached above");
2639        let from = match pen.rgb.take() {
2640            Some((fg, bg)) => {
2641                // What the pen holds, as parameters again.
2642                self.scratch.set_rgb(fg, bg);
2643                Some(&self.scratch)
2644            }
2645            None => held_codes(&self.codes, pen.held),
2646        };
2647        // Styles that differ only in their link share colours: no SGR
2648        // (`write_from` writes nothing for parameters already in force).
2649        codes.write_from(from, out);
2650        pen.held = Held::Style(style);
2651        pen.style = Some(style);
2652    }
2653}
2654
2655/// The SGR that changes a true-colour pen from `old` to `new`: only the
2656/// colours that differ (a colour gone is its default, 39 or 49). `sgr` is
2657/// `new`'s, encoded.
2658fn rgb_delta(
2659    old: (Option<Rgb>, Option<Rgb>),
2660    new: (Option<Rgb>, Option<Rgb>),
2661    sgr: RgbSgr<'_>,
2662    out: &mut String,
2663) {
2664    let fg = old.0 != new.0;
2665    let bg = old.1 != new.1;
2666    if fg && bg && new.0.is_some() && new.1.is_some() {
2667        out.push_str(sgr.whole);
2668        return;
2669    }
2670    out.push_str("\x1b[");
2671    let mut first = true;
2672    for (changed, now, code, default) in [(fg, new.0, sgr.fg, "39"), (bg, new.1, sgr.bg, "49")] {
2673        if !changed {
2674            continue;
2675        }
2676        if !first {
2677            out.push(';');
2678        }
2679        first = false;
2680        out.push_str(if now.is_some() { code } else { default });
2681    }
2682    out.push('m');
2683}
2684
2685/// `CSI n c`, with `n` left out when it is 1.
2686fn csi(out: &mut String, n: u16, c: char) {
2687    out.push_str("\x1b[");
2688    if n != 1 {
2689        push_u16(out, n);
2690    }
2691    out.push(c);
2692}
2693
2694/// An absolute move to `x`, `y` (CUP), its column left out when it is the
2695/// first.
2696fn push_cup(out: &mut String, x: u16, y: u16) {
2697    out.push_str("\x1b[");
2698    if x > 0 || y > 0 {
2699        push_u16(out, y + 1);
2700        if x > 0 {
2701            out.push(';');
2702            push_u16(out, x + 1);
2703        }
2704    }
2705    out.push('H');
2706}
2707
2708/// The decimal digits of `n`.
2709fn digits(n: u16) -> usize {
2710    match n {
2711        0..=9 => 1,
2712        10..=99 => 2,
2713        100..=999 => 3,
2714        1000..=9999 => 4,
2715        _ => 5,
2716    }
2717}
2718
2719/// `n` in decimal.
2720fn push_u16(out: &mut String, n: u16) {
2721    let mut buffer = [0u8; 5];
2722    let mut at = buffer.len();
2723    let mut n = n;
2724    loop {
2725        at -= 1;
2726        buffer[at] = b'0' + (n % 10) as u8;
2727        n /= 10;
2728        if n == 0 {
2729            break;
2730        }
2731    }
2732    for &b in &buffer[at..] {
2733        out.push(char::from(b));
2734    }
2735}
2736
2737/// `lead` and a true colour's parameters (`38;2;r;g;b`).
2738fn rgb_code(out: &mut String, lead: &str, rgb: Rgb) {
2739    out.push_str(lead);
2740    for (i, part) in rgb.into_iter().enumerate() {
2741        if i > 0 {
2742            out.push(';');
2743        }
2744        push_u8(out, part);
2745    }
2746}
2747
2748/// `n` in decimal.
2749fn push_u8(out: &mut String, n: u8) {
2750    if n >= 100 {
2751        out.push(char::from(b'0' + n / 100));
2752    }
2753    if n >= 10 {
2754        out.push(char::from(b'0' + n / 10 % 10));
2755    }
2756    out.push(char::from(b'0' + n % 10));
2757}
2758
2759/// The SGR a [`Painter`] wrote to change its pen from one cached style's
2760/// parameters to another's, by the two ids, in one buffer. Emptied with
2761/// the parameters' cache, and when it holds more than [`SgrChanges::MAX`]
2762/// bytes.
2763#[derive(Default)]
2764struct SgrChanges {
2765    text: String,
2766    at: HashMap<(u32, u32), (u32, u32), FxBuild>,
2767}
2768
2769impl SgrChanges {
2770    const MAX: usize = 1 << 16;
2771
2772    fn clear(&mut self) {
2773        self.text.clear();
2774        self.at.clear();
2775    }
2776
2777    fn get(&self, from: StyleId, to: StyleId) -> Option<&str> {
2778        let &(start, end) = self.at.get(&(from.index() as u32, to.index() as u32))?;
2779        self.text.get(start as usize..end as usize)
2780    }
2781
2782    fn put(&mut self, from: StyleId, to: StyleId, change: &str) {
2783        if self.text.len() + change.len() > SgrChanges::MAX {
2784            self.clear();
2785        }
2786        let start = self.text.len() as u32;
2787        self.text.push_str(change);
2788        let key = (from.index() as u32, to.index() as u32);
2789        self.at.insert(key, (start, self.text.len() as u32));
2790    }
2791}
2792
2793/// A true-colour pair's SGR, encoded once per style id: where it starts
2794/// in [`RgbCodes::text`], and how long the foreground's and the
2795/// background's parameters are.
2796#[derive(Clone, Copy)]
2797struct RgbAt {
2798    at: u32,
2799    fg: u8,
2800    bg: u8,
2801}
2802
2803/// The encoded SGR of the true-colour pairs a painter has written, by
2804/// style id: a colour frame repeats its pairs, so each is formatted once,
2805/// not once a cell. Each is kept whole (`ESC [ 38;2;r;g;b ; 48;2;r;g;b m`),
2806/// so a change of both colours is one copy, and either colour's
2807/// parameters are a slice of it.
2808#[derive(Default)]
2809struct RgbCodes {
2810    text: String,
2811    at: Vec<Option<RgbAt>>,
2812}
2813
2814/// What [`RgbCodes::get`] hands back: the whole SGR, and the foreground's
2815/// and the background's parameters (empty when missing).
2816struct RgbSgr<'a> {
2817    whole: &'a str,
2818    fg: &'a str,
2819    bg: &'a str,
2820}
2821
2822impl RgbCodes {
2823    fn clear(&mut self) {
2824        self.text.clear();
2825        self.at.clear();
2826    }
2827
2828    /// Style `id`'s SGR, its colours `pair`.
2829    fn get(&mut self, id: StyleId, pair: (Option<Rgb>, Option<Rgb>)) -> RgbSgr<'_> {
2830        if self.at.len() <= id.index() {
2831            self.at.resize(id.index() + 1, None);
2832        }
2833        let at = match self.at[id.index()] {
2834            Some(at) => at,
2835            None => {
2836                let start = self.text.len();
2837                self.text.push_str("\x1b[");
2838                if let Some(fg) = pair.0 {
2839                    rgb_code(&mut self.text, "38;2;", fg);
2840                }
2841                let middle = self.text.len();
2842                if pair.0.is_some() && pair.1.is_some() {
2843                    self.text.push(';');
2844                }
2845                let second = self.text.len();
2846                if let Some(bg) = pair.1 {
2847                    rgb_code(&mut self.text, "48;2;", bg);
2848                }
2849                let end = self.text.len();
2850                self.text.push('m');
2851                let at = RgbAt {
2852                    at: start as u32,
2853                    fg: (middle - start - 2) as u8,
2854                    bg: (end - second) as u8,
2855                };
2856                self.at[id.index()] = Some(at);
2857                at
2858            }
2859        };
2860        let start = at.at as usize;
2861        let fg = start + 2..start + 2 + at.fg as usize;
2862        let joint = usize::from(at.fg > 0 && at.bg > 0);
2863        let bg = fg.end + joint..fg.end + joint + at.bg as usize;
2864        RgbSgr {
2865            whole: &self.text[start..bg.end + 1],
2866            fg: &self.text[fg],
2867            bg: &self.text[bg],
2868        }
2869    }
2870}
2871
2872/// For each row, the merged column spans the rectangles cover.
2873fn row_spans(rects: &[Rect], height: u16) -> Vec<(u16, Vec<(u16, u16)>)> {
2874    let mut rows: Vec<Vec<(u16, u16)>> = vec![Vec::new(); height as usize];
2875    for rect in rects {
2876        for y in rect.y..rect.bottom().min(height) {
2877            rows[y as usize].push((rect.x, rect.right()));
2878        }
2879    }
2880    rows.into_iter()
2881        .enumerate()
2882        .filter(|(_, spans)| !spans.is_empty())
2883        .map(|(y, mut spans)| {
2884            spans.sort_unstable();
2885            let mut merged: Vec<(u16, u16)> = Vec::new();
2886            for (a, b) in spans {
2887                match merged.last_mut() {
2888                    Some(last) if a <= last.1 => last.1 = last.1.max(b),
2889                    _ => merged.push((a, b)),
2890                }
2891            }
2892            (y as u16, merged)
2893        })
2894        .collect()
2895}
2896
2897#[cfg(test)]
2898mod tests {
2899    use super::*;
2900
2901    fn line(markup: &str) -> Vec<Segment> {
2902        let console = rich::Console::builder()
2903            .width(80)
2904            .color_system(None)
2905            .build();
2906        let text = rich::Text::from_markup(markup).unwrap();
2907        console
2908            .render_lines(&text, &console.options(), false)
2909            .remove(0)
2910    }
2911
2912    #[test]
2913    fn lines_are_written_into_their_rectangle_only() {
2914        let mut screen = Screen::new(10, 3);
2915        screen.write_lines(
2916            Rect::new(0, 0, 10, 3),
2917            &[line("xxxxxxxxxx"), line("xxxxxxxxxx")],
2918        );
2919        screen.write_lines(Rect::new(2, 1, 4, 2), &[line("abcdefgh")]);
2920        assert_eq!(screen.plain(), ["xxxxxxxxxx", "xxabcdxxxx", "          "]);
2921    }
2922
2923    #[test]
2924    fn wide_graphemes_take_two_cells_and_never_cross_the_edge() {
2925        let mut screen = Screen::new(5, 1);
2926        screen.write_lines(Rect::new(0, 0, 5, 1), &[line("日本語")]);
2927        assert_eq!(screen.plain(), ["日本 "]);
2928        assert!(screen.cell(1, 0).is_continuation());
2929        assert_eq!(screen.cell(4, 0).text(), " ");
2930    }
2931
2932    #[test]
2933    fn simple_lines_write_as_a_frame_would() {
2934        let pieces = [
2935            "ab",
2936            "日本",
2937            "●x",
2938            "⣿⠁",
2939            " ",
2940            "é",
2941            "a\u{301}",
2942            "\x1b",
2943            "─┐",
2944            "\u{301}x",
2945            "👨\u{200d}👩\u{200d}👧",
2946            "❤\u{fe0f}",
2947            "\u{200d}",
2948            "e\u{301}\u{302}",
2949        ];
2950        let styles = [
2951            None,
2952            Style::parse("bold red").ok(),
2953            Style::parse("on blue").ok(),
2954        ];
2955        let mut n = 7usize;
2956        for case in 0..1500 {
2957            let lines: Vec<Vec<Segment>> = (0..3)
2958                .map(|_| {
2959                    (0..4)
2960                        .map(|_| {
2961                            n = n.wrapping_mul(1103515245).wrapping_add(12345) >> 3;
2962                            let text = pieces[n % pieces.len()];
2963                            Segment::new(text, styles[n / 9 % 3].clone())
2964                        })
2965                        .collect()
2966                })
2967                .collect();
2968            let rect = Rect::new((case % 3) as u16, 1, 5 + (case % 4) as u16, 3);
2969            let mut fast = Screen::new(12, 5);
2970            fast.write_lines(Rect::new(0, 0, 12, 5), &[line("日本語日本語")]);
2971            let mut slow = fast.clone();
2972            fast.write_lines(rect, &lines);
2973            slow.split_edges(rect.intersection(slow.area()));
2974            slow.write_frame_lines(rect.intersection(slow.area()), &lines);
2975            assert_eq!(fast.lines(), slow.lines(), "{lines:?} in {rect:?}");
2976            let widths = |s: &Screen| (0..12).map(|x| s.cell(x, 1).width).collect::<Vec<_>>();
2977            assert_eq!(widths(&fast), widths(&slow), "{lines:?}");
2978        }
2979    }
2980
2981    #[test]
2982    fn controls_in_text_become_visible() {
2983        let mut screen = Screen::new(6, 1);
2984        let evil = vec![Segment::new("a\x1b[2Jb", None)];
2985        screen.write_lines(Rect::new(0, 0, 6, 1), &[evil]);
2986        assert_eq!(screen.plain(), ["a␛[2Jb"]);
2987    }
2988
2989    #[test]
2990    fn the_painter_writes_only_changed_cells_inside_the_damage() {
2991        let mut screen = Screen::new(20, 2);
2992        let mut painter = Painter::new(Some(ColorSystem::Truecolor));
2993        screen.write_lines(screen.area(), &[line("tick 1"), line("[bold red]alert[/]")]);
2994        let first = painter.paint(&screen, &[]);
2995        assert!(first.contains("\x1b[2J") && first.contains("alert"));
2996        screen.write_lines(Rect::new(0, 0, 20, 1), &[line("tick 2")]);
2997        let second = painter.paint(&screen, &[Rect::new(0, 0, 20, 1)]);
2998        // One cell changed: move there, write "2".
2999        assert_eq!(second, "\x1b[1;6H\x1b[0m2");
3000        // Nothing damaged, nothing written.
3001        assert_eq!(painter.paint(&screen, &[]), "");
3002    }
3003
3004    #[test]
3005    fn styles_and_links_carry_across_a_run() {
3006        let mut screen = Screen::new(12, 1);
3007        let mut painter = Painter::new(Some(ColorSystem::Truecolor));
3008        painter.paint(&screen, &[]);
3009        screen.write_lines(
3010            screen.area(),
3011            &[line("[link=https://x.y]docs[/link] [b]ok[/b]")],
3012        );
3013        let out = painter.paint(&screen, &[screen.area()]);
3014        assert!(
3015            out.starts_with("\x1b[H\x1b]8;;https://x.y\x1b\\"),
3016            "{out:?}"
3017        );
3018        // One SGR for the four link cells, not four.
3019        assert_eq!(out.matches("\x1b]8;;https://x.y").count(), 1, "{out:?}");
3020        assert!(
3021            out.contains("\x1b]8;;\x1b\\"),
3022            "the link is closed: {out:?}"
3023        );
3024        assert!(out.contains("\x1b[0;1mok"), "{out:?}");
3025    }
3026
3027    #[test]
3028    fn a_wide_grapheme_cut_by_the_destination_edge_becomes_a_space() {
3029        let mut source = Screen::new(4, 1);
3030        source.write_lines(source.area(), &[line("ab日")]);
3031        let mut narrow = Screen::new(3, 1);
3032        narrow.blit(&source, source.area(), 0, 0);
3033        assert_eq!(narrow.plain(), ["ab "]);
3034        assert_eq!(narrow.cell(2, 0).width, 1);
3035    }
3036
3037    #[test]
3038    fn the_painter_reports_the_runs_it_wrote() {
3039        let mut screen = Screen::new(10, 2);
3040        let mut painter = Painter::new(None);
3041        screen.write_lines(screen.area(), &[line("abc"), line("xyz")]);
3042        painter.paint(&screen, &[]);
3043        screen.write_lines(Rect::new(0, 1, 10, 1), &[line("xYz")]);
3044        painter.paint(&screen, &[Rect::new(0, 0, 10, 2)]);
3045        assert_eq!(painter.written(), [Rect::new(1, 1, 1, 1)]);
3046    }
3047
3048    #[test]
3049    fn a_wide_grapheme_replaced_by_narrow_text_clears_its_second_column() {
3050        let mut screen = Screen::new(4, 1);
3051        let mut painter = Painter::new(None);
3052        screen.write_lines(screen.area(), &[line("日x")]);
3053        painter.paint(&screen, &[]);
3054        screen.write_lines(screen.area(), &[line("abx")]);
3055        let out = painter.paint(&screen, &[screen.area()]);
3056        assert_eq!(out, "\x1b[H\x1b[0mab");
3057    }
3058
3059    /// A small, fast generator: the same noise on every run.
3060    struct Noise(u64);
3061
3062    impl Noise {
3063        fn rgb(&mut self) -> [u8; 3] {
3064            self.0 = self
3065                .0
3066                .wrapping_mul(6364136223846793005)
3067                .wrapping_add(1442695040888963407);
3068            let [r, g, b, _] = ((self.0 >> 33) as u32).to_le_bytes();
3069            [r, g, b]
3070        }
3071    }
3072
3073    /// A frame of `screen` in random colour pairs, as `put_rgb` cells, or
3074    /// as rendered segments when `segments`.
3075    fn noise_frame(screen: &mut Screen, noise: &mut Noise, segments: bool) {
3076        let area = screen.area();
3077        if segments {
3078            let lines: Vec<Vec<Segment>> = (0..area.height)
3079                .map(|_| {
3080                    (0..area.width)
3081                        .map(|_| {
3082                            let [r, g, b] = noise.rgb();
3083                            let [r2, g2, b2] = noise.rgb();
3084                            let style = Style::from_color(
3085                                Some(rich::Color::from_rgb(r, g, b)),
3086                                Some(rich::Color::from_rgb(r2, g2, b2)),
3087                            );
3088                            Segment::new("▀", Some(style))
3089                        })
3090                        .collect()
3091                })
3092                .collect();
3093            screen.write_lines(area, &lines);
3094        } else {
3095            for y in 0..area.height {
3096                for x in 0..area.width {
3097                    let (fg, bg) = (noise.rgb(), noise.rgb());
3098                    screen.put_rgb(x, y, '▀', Some(fg), Some(bg));
3099                }
3100            }
3101        }
3102    }
3103
3104    #[test]
3105    fn the_style_table_stays_bounded_over_a_thousand_random_frames() {
3106        for segments in [false, true] {
3107            let mut screen = Screen::new(40, 12);
3108            let mut painter = Painter::new(Some(ColorSystem::Truecolor));
3109            let mut noise = Noise(1);
3110            let mut most = 0;
3111            for _ in 0..1000 {
3112                noise_frame(&mut screen, &mut noise, segments);
3113                painter.paint(&screen, &[screen.area()]);
3114                painter.compact(&mut screen);
3115                most = most.max(screen.style_count());
3116            }
3117            // 480 cells, each a new pair a frame: about a frame's worth is
3118            // kept, never a thousand frames'.
3119            assert!(most <= 2 * 1024 + 1, "{most} styles ({segments})");
3120            assert!(painter.codes.len() <= most, "{}", painter.codes.len());
3121        }
3122    }
3123
3124    #[test]
3125    fn compacting_changes_no_byte_painted() {
3126        // Two screens drawn alike, one compacted after every paint and one
3127        // never: the painters write the same bytes, frame after frame.
3128        let mut kept = (
3129            Screen::new(30, 6),
3130            Painter::new(Some(ColorSystem::Truecolor)),
3131        );
3132        let mut compacted = (
3133            Screen::new(30, 6),
3134            Painter::new(Some(ColorSystem::Truecolor)),
3135        );
3136        let mut noise = Noise(9);
3137        for frame in 0..60 {
3138            let mut lines = vec![
3139                line("[bold]title[/] [link=https://x.y]docs[/link]"),
3140                line(&format!("[red]{frame}[/] [on blue]  [/]")),
3141            ];
3142            if frame % 3 == 0 {
3143                lines.push(line("[italic green]every third[/]"));
3144            }
3145            let mut cells = Vec::new();
3146            for x in 0..30 {
3147                cells.push((x, noise.rgb(), noise.rgb()));
3148            }
3149            let underlay = Style::parse("on #202020").unwrap();
3150            let mut bytes = Vec::new();
3151            for (screen, painter) in [&mut kept, &mut compacted] {
3152                screen.write_lines(Rect::new(0, 0, 30, 3), &lines);
3153                // Only some cells change colour, so most are not repainted.
3154                for &(x, fg, bg) in &cells[..(frame % 30)] {
3155                    screen.put_rgb(x, 4, '▀', Some(fg), Some(bg));
3156                }
3157                for x in 0..30 {
3158                    screen.underlay(x, 1, &underlay);
3159                }
3160                bytes.push(painter.paint(screen, &[Rect::new(0, 0, 30, 5)]));
3161            }
3162            let remap = compacted.0.compact_styles();
3163            compacted.1.remap_styles(&remap);
3164            assert_eq!(bytes[0], bytes[1], "frame {frame}");
3165            assert_eq!(kept.0.plain(), compacted.0.plain());
3166        }
3167        assert!(compacted.0.style_count() < kept.0.style_count());
3168    }
3169
3170    #[test]
3171    fn a_cell_sent_in_a_dropped_style_reads_as_changed() {
3172        let mut screen = Screen::new(2, 1);
3173        let mut painter = Painter::new(Some(ColorSystem::Truecolor));
3174        screen.put_rgb(0, 0, 'a', Some([1, 2, 3]), None);
3175        painter.paint(&screen, &[]);
3176        // Drawn over without a paint, then compacted: the style sent went.
3177        screen.put_rgb(0, 0, 'a', Some([4, 5, 6]), None);
3178        let remap = screen.compact_styles();
3179        painter.remap_styles(&remap);
3180        let out = painter.paint(&screen, &[screen.area()]);
3181        assert_eq!(out, "\x1b[H\x1b[0;38;2;4;5;6ma\x1b[0m");
3182    }
3183
3184    #[test]
3185    fn pixel_cells_write_what_their_style_would() {
3186        let mut noise = Noise(3);
3187        for system in [
3188            ColorSystem::Truecolor,
3189            ColorSystem::EightBit,
3190            ColorSystem::Standard,
3191        ] {
3192            for case in 0..200 {
3193                let fg = (case % 3 != 0).then(|| noise.rgb());
3194                let bg = (case % 5 != 0).then(|| noise.rgb());
3195                let mut fast = Screen::new(1, 1);
3196                fast.put_rgb(0, 0, '▄', fg, bg);
3197                let mut slow = Screen::new(1, 1);
3198                let style = Style::from_color(
3199                    fg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
3200                    bg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
3201                );
3202                slow.write_lines(slow.area(), &[vec![Segment::new("▄", Some(style))]]);
3203                let paint = |screen: &Screen| Painter::new(Some(system)).paint(screen, &[]);
3204                assert_eq!(paint(&fast), paint(&slow), "{fg:?} {bg:?} {system:?}");
3205            }
3206        }
3207    }
3208
3209    #[test]
3210    fn a_pixel_cell_over_half_a_wide_grapheme_clears_the_other_half() {
3211        let mut screen = Screen::new(5, 1);
3212        screen.write_lines(screen.area(), &[line("日本x")]);
3213        screen.put_rgb(1, 0, '▀', Some([255, 0, 0]), None);
3214        screen.put_rgb(2, 0, '▀', None, Some([0, 0, 255]));
3215        assert_eq!(screen.plain(), [" ▀▀ x"]);
3216        assert!((0..5).all(|x| screen.cell(x, 0).width == 1));
3217        // Wide, zero-width and control characters take one cell.
3218        screen.put_rgb(0, 0, '日', None, None);
3219        screen.put_rgb(1, 0, '\u{301}', None, None);
3220        screen.put_rgb(2, 0, '\x1b', None, None);
3221        assert_eq!(screen.plain(), ["  ␛ x"]);
3222        // Off the screen, nothing.
3223        screen.put_rgb(5, 0, 'z', None, None);
3224        screen.put(0, 1, 'z', StyleId::NONE);
3225        assert_eq!(screen.plain(), ["  ␛ x"]);
3226    }
3227
3228    #[test]
3229    fn the_pixel_glyphs_are_one_cell_wide() {
3230        let ranges = [
3231            (' ', '~'),
3232            ('\u{2500}', '\u{259f}'),
3233            ('\u{2800}', '\u{28ff}'),
3234        ];
3235        for (first, last) in ranges {
3236            for c in first..=last {
3237                assert_eq!(rich::cells::cell_len(&c.to_string()), 1, "{c:?}");
3238            }
3239        }
3240    }
3241
3242    #[test]
3243    fn layering_a_style_over_many_cells_combines_each_pair_once() {
3244        let mut screen = Screen::new(8, 1);
3245        screen.write_lines(screen.area(), &[line("[red]abcd[/][blue]efgh[/]")]);
3246        let before = screen.style_count();
3247        let under = Style::parse("on black").unwrap();
3248        for x in 0..8 {
3249            screen.underlay(x, 0, &under);
3250        }
3251        // The layer, and red and blue on black.
3252        assert_eq!(screen.style_count(), before + 3);
3253        let style_of = |screen: &Screen, x| screen.style(screen.cell(x, 0).style).cloned();
3254        let parse = |s| Style::parse(s).ok();
3255        assert_eq!(
3256            (style_of(&screen, 0), style_of(&screen, 7)),
3257            (parse("red on black"), parse("blue on black"))
3258        );
3259        let over = Style::parse("bold").unwrap();
3260        screen.restyle(0, 0, &over);
3261        assert_eq!(style_of(&screen, 0), parse("bold red on black"));
3262        screen.compact_styles();
3263        screen.underlay(1, 0, &under);
3264        assert_eq!(style_of(&screen, 1), parse("red on black"));
3265    }
3266
3267    #[test]
3268    fn a_blit_translates_each_style_once() {
3269        let mut source = Screen::new(6, 1);
3270        source.write_lines(source.area(), &[line("[bold]ab[/][red]cd[/]ef")]);
3271        let mut target = Screen::new(6, 1);
3272        target.blit(&source, source.area(), 0, 0);
3273        assert_eq!(target.lines(), source.lines());
3274        assert_eq!(target.style_count(), source.style_count());
3275    }
3276}