Skip to main content

rich_ext/
frame.rs

1//! Frames: a rendered result as rows of styled runs (#226).
2//!
3//! A [`Frame`] is built from the segment stream core already returns, so no
4//! renderable changes. It keeps that stream's shape (one [`Run`] per segment
5//! piece) over one text arena, with every style stored once in a
6//! [`StyleTable`]. From it:
7//!
8//! - [`Frame::to_ansi`] writes exactly what `Console::segments_to_string`
9//!   writes for the same (control-free) stream, byte for byte;
10//! - [`Frame::to_ansi_merged`] merges adjacent runs of one style first, which
11//!   is smaller but not upstream's bytes (opt-in, for live repaint and our own
12//!   exports);
13//! - [`Frame::cells`] derives one cell per grapheme on demand, with core's
14//!   widths;
15//! - [`Frame::diff`] lists the cells that changed against a previous frame.
16//!
17//! Control segments are dropped: a frame holds content, and whatever paints
18//! it owns cursor movement. See `docs/design/render-tree.md`. Terminal
19//! graphics travel beside the cells as [`Placement`]s (see [`graphics`]).
20//!
21//! A frame can also carry semantic [`Region`]s (see [`regions`]) and export
22//! itself as HTML or SVG with links kept ([`Frame::to_html`],
23//! [`Frame::to_svg`]).
24
25mod export;
26pub mod graphics;
27pub mod regions;
28
29pub use export::{HtmlOptions, SvgOptions};
30pub use graphics::{Graphic, GraphicsPlan, Placement, PlacementSource, plan as plan_graphics};
31pub use regions::{Rect, Region, RegionRecorder, Span, render_frame, role_name};
32
33use std::collections::HashMap;
34use std::hash::{BuildHasherDefault, Hasher};
35use std::ops::Range;
36
37use rich::cells::{cell_len, split_graphemes};
38use rich::protocol::RegionId;
39use rich::{ColorSystem, Console, Segment, Style};
40
41/// An index into a [`StyleTable`].
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub struct StyleId(u32);
44
45impl StyleId {
46    /// "No style": a segment whose `style` is `None`.
47    pub const NONE: StyleId = StyleId(0);
48
49    /// No style's id: what a [`StyleRemap`] gives a dropped style, so a
50    /// copy of cells that still holds it differs from every cell.
51    pub const GONE: StyleId = StyleId(u32::MAX);
52
53    pub fn index(self) -> usize {
54        self.0 as usize
55    }
56
57    pub(crate) fn from_index(index: usize) -> StyleId {
58        StyleId(index as u32)
59    }
60}
61
62/// A colour as three bytes: red, green and blue.
63pub type Rgb = [u8; 3];
64
65/// Styles, stored once each. Runs refer to them by [`StyleId`].
66///
67/// Interning hashes a style's public parts (colours, attributes and link)
68/// with a fast non-cryptographic hash and confirms a match with `==`, so
69/// metadata still tells styles apart. A style made of two plain colours can
70/// be interned by the colours alone ([`intern_rgb`](Self::intern_rgb)),
71/// without building a [`Style`] unless it is new.
72///
73/// The table only grows while it is used; a table kept between frames (a
74/// [`Screen`](crate::surface::Screen)'s) drops what its cells no longer
75/// use with [`retain`](Self::retain).
76#[derive(Clone, Debug)]
77pub struct StyleTable {
78    styles: Vec<Option<Style>>,
79    /// Each style's key (see [`style_key`]), to rebuild the index.
80    keys: Vec<u64>,
81    /// Each style's colour pair when it was interned as one (see
82    /// [`pair_key`]), else 0.
83    pairs: Vec<u64>,
84    /// Per id, the next id whose key is the same (0 ends the chain:
85    /// [`StyleId::NONE`] is never in one).
86    chain: Vec<u32>,
87    /// The first id with each key.
88    index: HashMap<u64, u32, FxBuild>,
89    /// Colour pairs interned with [`intern_rgb`](Self::intern_rgb).
90    rgb: HashMap<u64, StyleId, FxBuild>,
91    /// The last style interned: consecutive segments usually share one.
92    last: StyleId,
93    /// How many styles the last [`retain`](Self::retain) kept.
94    kept: usize,
95}
96
97impl Default for StyleTable {
98    fn default() -> Self {
99        StyleTable {
100            styles: vec![None],
101            keys: vec![0],
102            pairs: vec![0],
103            chain: vec![0],
104            index: HashMap::default(),
105            rgb: HashMap::default(),
106            last: StyleId::NONE,
107            kept: 1,
108        }
109    }
110}
111
112impl StyleTable {
113    pub fn new() -> Self {
114        StyleTable::default()
115    }
116
117    /// The id of `style`, adding it if it is new. `None` is [`StyleId::NONE`].
118    pub fn intern(&mut self, style: Option<&Style>) -> StyleId {
119        let Some(style) = style else {
120            return StyleId::NONE;
121        };
122        if self.styles[self.last.index()].as_ref() == Some(style) {
123            return self.last;
124        }
125        let key = style_key(style);
126        let id = match self.find(key, style) {
127            Some(id) => id,
128            None => self.push(key, style.clone()),
129        };
130        self.last = id;
131        id
132    }
133
134    /// The id of a style of foreground `fg` and background `bg` in true
135    /// colour (either missing), as [`Style::from_color`] with
136    /// [`Color::from_rgb`](rich::Color::from_rgb) would make it; neither is
137    /// [`StyleId::NONE`]. A pixel's cell: the style is built only the first
138    /// time the pair is seen.
139    ///
140    /// ```
141    /// use rich::{Color, Style};
142    /// use rich_ext::frame::StyleTable;
143    ///
144    /// let mut table = StyleTable::new();
145    /// let id = table.intern_rgb(Some([255, 0, 0]), None);
146    /// let red = Style::from_color(Some(Color::from_rgb(255, 0, 0)), None);
147    /// assert_eq!(table.intern(Some(&red)), id);
148    /// assert_eq!(table.rgb(id), Some((Some([255, 0, 0]), None)));
149    /// ```
150    pub fn intern_rgb(&mut self, fg: Option<Rgb>, bg: Option<Rgb>) -> StyleId {
151        let pair = pair_key(fg, bg);
152        if pair == 0 {
153            return StyleId::NONE;
154        }
155        if self.pairs[self.last.index()] == pair {
156            return self.last;
157        }
158        let id = match self.rgb.get(&pair) {
159            Some(&id) => id,
160            None => {
161                let style = Style::from_color(
162                    fg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
163                    bg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
164                );
165                let key = style_key(&style);
166                let id = match self.find(key, &style) {
167                    Some(id) => id,
168                    None => self.push(key, style),
169                };
170                self.pairs[id.index()] = pair;
171                self.rgb.insert(pair, id);
172                id
173            }
174        };
175        self.last = id;
176        id
177    }
178
179    /// The colours of a style interned with [`intern_rgb`](Self::intern_rgb)
180    /// (or found by it): what a painter can write without asking the style.
181    pub fn rgb(&self, id: StyleId) -> Option<(Option<Rgb>, Option<Rgb>)> {
182        match self.pairs.get(id.index()).copied().unwrap_or(0) {
183            0 => None,
184            pair => Some((unpack(pair >> 32), unpack(pair))),
185        }
186    }
187
188    fn find(&self, key: u64, style: &Style) -> Option<StyleId> {
189        let mut at = self.index.get(&key).copied().unwrap_or(0);
190        while at != 0 {
191            if self.styles[at as usize].as_ref() == Some(style) {
192                return Some(StyleId(at));
193            }
194            at = self.chain[at as usize];
195        }
196        None
197    }
198
199    fn push(&mut self, key: u64, style: Style) -> StyleId {
200        let id = self.styles.len() as u32;
201        // A style of true colours alone is a pair, whichever way it came:
202        // a painter writes it from the colours.
203        let pair = truecolor_pair(&style).unwrap_or(0);
204        self.styles.push(Some(style));
205        self.keys.push(key);
206        self.pairs.push(pair);
207        if pair != 0 {
208            self.rgb.entry(pair).or_insert(StyleId(id));
209        }
210        // At the head of its key's chain.
211        let next = self.index.insert(key, id).unwrap_or(0);
212        self.chain.push(next);
213        StyleId(id)
214    }
215
216    /// The style behind `id`; `None` for [`StyleId::NONE`].
217    pub fn get(&self, id: StyleId) -> Option<&Style> {
218        self.styles.get(id.index()).and_then(Option::as_ref)
219    }
220
221    /// The number of entries, counting [`StyleId::NONE`].
222    pub fn len(&self) -> usize {
223        self.styles.len()
224    }
225
226    /// Always false: the table always holds [`StyleId::NONE`].
227    pub fn is_empty(&self) -> bool {
228        false
229    }
230
231    /// Whether the table has at least doubled since the last
232    /// [`retain`](Self::retain) and holds more than a few hundred styles:
233    /// when dropping the unused ones is worth a pass over the cells.
234    pub fn piled_up(&self) -> bool {
235        self.styles.len() > PILE.max(2 * self.kept)
236    }
237
238    /// Keep only the styles `keep` marks (by index; [`StyleId::NONE`] is
239    /// always kept, and ids past `keep`'s end are dropped), renumbered in
240    /// their order. The result maps the old ids to the new: everything
241    /// holding an old id must be moved over with it.
242    ///
243    /// ```
244    /// use rich::Style;
245    /// use rich_ext::frame::{StyleId, StyleTable};
246    ///
247    /// let mut table = StyleTable::new();
248    /// let bold = table.intern(Some(&Style::parse("bold").unwrap()));
249    /// let red = table.intern(Some(&Style::parse("red").unwrap()));
250    /// let mut keep = vec![false; table.len()];
251    /// keep[red.index()] = true;
252    /// let remap = table.retain(&keep);
253    /// assert_eq!(table.len(), 2);
254    /// assert_eq!(remap.get(bold), None);
255    /// let red = remap.get(red).unwrap();
256    /// assert_eq!(table.get(red), Some(&Style::parse("red").unwrap()));
257    /// assert_eq!(remap.get(StyleId::NONE), Some(StyleId::NONE));
258    /// ```
259    pub fn retain(&mut self, keep: &[bool]) -> StyleRemap {
260        let mut map = vec![StyleRemap::GONE; self.styles.len()];
261        map[0] = 0;
262        let mut next = 1usize;
263        for (old, new) in map.iter_mut().enumerate().skip(1) {
264            if !keep.get(old).copied().unwrap_or(false) {
265                continue;
266            }
267            *new = next as u32;
268            if old != next {
269                self.styles.swap(old, next);
270                self.keys[next] = self.keys[old];
271                self.pairs[next] = self.pairs[old];
272            }
273            next += 1;
274        }
275        self.styles.truncate(next);
276        self.keys.truncate(next);
277        self.pairs.truncate(next);
278        self.chain.clear();
279        self.chain.resize(next, 0);
280        self.index.clear();
281        self.rgb.clear();
282        for id in 1..next as u32 {
283            let key = self.keys[id as usize];
284            self.chain[id as usize] = self.index.insert(key, id).unwrap_or(0);
285            if self.pairs[id as usize] != 0 {
286                self.rgb.insert(self.pairs[id as usize], StyleId(id));
287            }
288        }
289        self.last = StyleId::NONE;
290        self.kept = next;
291        StyleRemap { map }
292    }
293}
294
295/// Below this many styles a table is not worth compacting.
296const PILE: usize = 1024;
297
298/// From the ids a [`StyleTable`] had to the ids it has after
299/// [`retain`](StyleTable::retain).
300#[derive(Clone, Debug, Default, PartialEq, Eq)]
301pub struct StyleRemap {
302    map: Vec<u32>,
303}
304
305impl StyleRemap {
306    const GONE: u32 = u32::MAX;
307
308    /// The new id of `old`; `None` when its style was dropped (or `old` was
309    /// never in the table).
310    pub fn get(&self, old: StyleId) -> Option<StyleId> {
311        match self.map.get(old.index()).copied() {
312            None | Some(StyleRemap::GONE) => None,
313            Some(new) => Some(StyleId(new)),
314        }
315    }
316
317    /// The new id of `old`, or an id no style has (that equals no id the
318    /// table gives) when its style was dropped: for a copy of cells that
319    /// must read as changed wherever the style went away.
320    pub fn get_or_gone(&self, old: StyleId) -> StyleId {
321        self.get(old).unwrap_or(StyleId::GONE)
322    }
323}
324
325/// The key a style is interned by: its colours (without the names they
326/// were spelled with), attributes and link.
327fn style_key(style: &Style) -> u64 {
328    let mut hasher = FxHasher::default();
329    hash_color(style.color(), &mut hasher);
330    hash_color(style.bgcolor(), &mut hasher);
331    let mut attrs = 0u32;
332    for index in 0..13 {
333        attrs |= match style.attr(index) {
334            None => 0,
335            Some(false) => 1,
336            Some(true) => 2,
337        } << (2 * index);
338    }
339    hasher.write_u32(attrs);
340    if let Some(link) = style.link() {
341        hasher.write(link.as_bytes());
342    }
343    hasher.finish()
344}
345
346fn hash_color(color: Option<&rich::Color>, hasher: &mut FxHasher) {
347    match color {
348        None => hasher.write_u32(0),
349        Some(color) => {
350            let triplet = color
351                .triplet
352                .map_or(0, |t| u32::from_be_bytes([0, t.red, t.green, t.blue]));
353            hasher.write_u64(
354                (color.kind as u64 + 1) << 40
355                    | u64::from(color.number.map_or(0, |n| n as u32 + 1)) << 24
356                    | u64::from(triplet),
357            );
358        }
359    }
360}
361
362/// A colour pair as a key: each colour in 25 bits, a bit for "present"
363/// above its 24 bits of RGB, the foreground in the high half. 0 is "no
364/// colour at all".
365/// The pair key of a style that sets a true-colour foreground, background
366/// or both, and nothing else.
367fn truecolor_pair(style: &Style) -> Option<u64> {
368    let rgb = |color: Option<&rich::Color>| match color {
369        None => Some(None),
370        Some(c) if c.kind == rich::color::ColorType::Truecolor => {
371            c.triplet.map(|t| Some([t.red, t.green, t.blue]))
372        }
373        Some(_) => None,
374    };
375    let (fg, bg) = (rgb(style.color())?, rgb(style.bgcolor())?);
376    if fg.is_none() && bg.is_none() {
377        return None;
378    }
379    let plain = Style::from_color(style.color().cloned(), style.bgcolor().cloned());
380    (plain == *style).then(|| pair_key(fg, bg))
381}
382
383/// Whether every character of `text` is its own grapheme, one or two
384/// cells wide: no zero-width marks, joiners or selectors to group.
385fn simple_text(text: &str) -> bool {
386    text.chars()
387        .all(|c| matches!(crate::surface::char_width(c), 1 | 2) && c != '\u{fe0f}')
388}
389
390/// `text`'s width in cells, as rich's `cell_len` gives it, without its
391/// table search for the characters most text is made of.
392fn text_width(text: &str) -> usize {
393    if text.contains(['\u{200d}', '\u{fe0f}']) {
394        return cell_len(text);
395    }
396    text.chars().map(crate::surface::char_width).sum()
397}
398
399fn pair_key(fg: Option<Rgb>, bg: Option<Rgb>) -> u64 {
400    let pack = |c: Option<Rgb>| {
401        c.map_or(0, |[r, g, b]| {
402            1 << 24 | u64::from(u32::from_be_bytes([0, r, g, b]))
403        })
404    };
405    pack(fg) << 32 | pack(bg)
406}
407
408fn unpack(half: u64) -> Option<Rgb> {
409    let half = half as u32;
410    (half & 1 << 24 != 0).then(|| {
411        let [_, r, g, b] = half.to_be_bytes();
412        [r, g, b]
413    })
414}
415
416/// The Firefox / rustc hash: a multiply and a rotate a word, much faster
417/// than the standard library's SipHash for short keys an attacker does not
418/// choose (style keys, colour pairs).
419#[derive(Clone, Copy, Default)]
420pub(crate) struct FxHasher {
421    hash: u64,
422}
423
424impl FxHasher {
425    const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
426
427    #[inline]
428    fn add(&mut self, word: u64) {
429        self.hash = (self.hash.rotate_left(5) ^ word).wrapping_mul(FxHasher::SEED);
430    }
431}
432
433impl Hasher for FxHasher {
434    #[inline]
435    fn write(&mut self, bytes: &[u8]) {
436        let (words, tail) = bytes.as_chunks::<8>();
437        for word in words {
438            self.add(u64::from_le_bytes(*word));
439        }
440        let mut rest = [0u8; 8];
441        rest[..tail.len()].copy_from_slice(tail);
442        self.add(u64::from_le_bytes(rest) ^ (tail.len() as u64) << 56);
443    }
444
445    #[inline]
446    fn write_u8(&mut self, n: u8) {
447        self.add(n.into());
448    }
449
450    #[inline]
451    fn write_u32(&mut self, n: u32) {
452        self.add(n.into());
453    }
454
455    #[inline]
456    fn write_u64(&mut self, n: u64) {
457        self.add(n);
458    }
459
460    #[inline]
461    fn write_usize(&mut self, n: usize) {
462        self.add(n as u64);
463    }
464
465    #[inline]
466    fn finish(&self) -> u64 {
467        self.hash
468    }
469}
470
471/// Builds [`FxHasher`]s, for `HashMap`s keyed by trusted values.
472pub(crate) type FxBuild = BuildHasherDefault<FxHasher>;
473
474/// The run is followed by a line break that belonged to its segment.
475const NEWLINE: u8 = 1;
476/// The run continues the previous run's segment after that line break.
477const JOIN: u8 = 2;
478
479/// A piece of one segment on one row: a slice of the frame's text, its width
480/// in cells and its style.
481#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub struct Run {
483    text: u32,
484    len: u32,
485    cells: u32,
486    style: StyleId,
487    flags: u8,
488}
489
490impl Run {
491    /// The width in terminal cells.
492    pub fn cells(&self) -> usize {
493        self.cells as usize
494    }
495
496    pub fn style(&self) -> StyleId {
497        self.style
498    }
499
500    fn range(&self) -> Range<usize> {
501        self.text as usize..(self.text + self.len) as usize
502    }
503}
504
505/// One terminal cell of a row, from [`Frame::cells`]. The cell after a wide
506/// grapheme is a continuation: empty text and width 0.
507#[derive(Clone, Copy, Debug, PartialEq, Eq)]
508pub struct Cell<'a> {
509    pub text: &'a str,
510    pub width: u8,
511    pub style: StyleId,
512}
513
514impl Cell<'_> {
515    pub fn is_continuation(&self) -> bool {
516        self.width == 0 && self.text.is_empty()
517    }
518}
519
520/// Cells `columns` of `row` differ between two frames.
521#[derive(Clone, Debug, PartialEq, Eq)]
522pub struct Change {
523    pub row: usize,
524    pub columns: Range<usize>,
525}
526
527/// Rows of styled runs, with interned styles. See the [module docs](self).
528#[derive(Clone, Debug, Default)]
529#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
530pub struct Frame {
531    /// Each row as a range of `runs`.
532    rows: Vec<Range<u32>>,
533    runs: Vec<Run>,
534    text: String,
535    styles: StyleTable,
536    /// The stream ended with a line break, so the last row is terminated.
537    trailing_newline: bool,
538    /// The region tags read off the segments' styles, each once.
539    tags: Vec<RegionId>,
540    /// Each tag's index in `tags`, so a lookup does not scan them.
541    tag_index: HashMap<RegionId, u32>,
542    /// Per run, 0 for no tag or an index into `tags` plus one. Empty when no
543    /// segment was tagged.
544    run_tags: Vec<u32>,
545    /// Regions from [`Frame::with_regions`], and each tag's place in them.
546    regions: Vec<Region>,
547    region_ids: HashMap<RegionId, usize>,
548    /// Graphics placed on the cells: the side channel (see [`graphics`]).
549    placements: Vec<Placement>,
550}
551
552impl Frame {
553    /// Build a frame from a segment stream, one run per piece of a segment
554    /// between line breaks. Control segments are dropped.
555    pub fn from_segments(segments: &[Segment]) -> Frame {
556        let mut frame = Frame {
557            runs: Vec::with_capacity(segments.len()),
558            text: String::with_capacity(segments.iter().map(|s| s.text.len()).sum()),
559            ..Frame::default()
560        };
561        let mut row_start = 0u32;
562        for segment in segments.iter().filter(|segment| !segment.control) {
563            // A region tag (metadata a region sink asked for) is kept beside
564            // the run, not in its style, so tagged and untagged text of one
565            // style still share a style.
566            let (style, tag) = match segment.style.as_ref() {
567                Some(style) if style.meta_ref().is_some() => match regions::split_region(style) {
568                    Some((id, bare)) => (frame.styles.intern(Some(&bare)), frame.tag(id)),
569                    None => (frame.styles.intern(Some(style)), 0),
570                },
571                other => (frame.styles.intern(other), 0),
572            };
573            let mut flags = 0;
574            let mut rest = segment.text.as_str();
575            loop {
576                match rest.find('\n') {
577                    Some(at) => {
578                        frame.push(&rest[..at], style, flags | NEWLINE, tag);
579                        let end = frame.runs.len() as u32;
580                        frame.rows.push(row_start..end);
581                        row_start = end;
582                        rest = &rest[at + 1..];
583                        flags = JOIN;
584                    }
585                    None => {
586                        if !rest.is_empty() {
587                            frame.push(rest, style, flags, tag);
588                        }
589                        break;
590                    }
591                }
592            }
593        }
594        let end = frame.runs.len() as u32;
595        if end > row_start {
596            frame.rows.push(row_start..end);
597        } else {
598            frame.trailing_newline = !frame.rows.is_empty();
599        }
600        frame
601    }
602
603    /// The index of `id` in `tags`, plus one.
604    fn tag(&mut self, id: RegionId) -> u32 {
605        let next = self.tags.len() as u32;
606        let at = *self.tag_index.entry(id).or_insert(next);
607        if at == next {
608            self.tags.push(id);
609        }
610        at + 1
611    }
612
613    fn push(&mut self, text: &str, style: StyleId, flags: u8, tag: u32) {
614        if tag != 0 && self.run_tags.len() < self.runs.len() {
615            self.run_tags.resize(self.runs.len(), 0);
616        }
617        if tag != 0 || !self.run_tags.is_empty() {
618            self.run_tags.push(tag);
619        }
620        let offset = self.text.len() as u32;
621        self.text.push_str(text);
622        self.runs.push(Run {
623            text: offset,
624            len: text.len() as u32,
625            cells: text_width(text) as u32,
626            style,
627            flags,
628        });
629    }
630
631    /// A copy with adjacent runs of one style on a row merged into one, and
632    /// empty runs dropped: what shows, not how it was split. Line breaks are
633    /// no longer part of any run, so [`Frame::to_ansi`] of the result writes
634    /// them unstyled.
635    pub fn merged(&self) -> Frame {
636        let mut frame = Frame {
637            runs: Vec::with_capacity(self.runs.len()),
638            text: String::with_capacity(self.text.len()),
639            styles: self.styles.clone(),
640            trailing_newline: self.trailing_newline,
641            tags: self.tags.clone(),
642            tag_index: self.tag_index.clone(),
643            regions: self.regions.clone(),
644            region_ids: self.region_ids.clone(),
645            placements: self.placements.clone(),
646            ..Frame::default()
647        };
648        for row in 0..self.height() {
649            let start = frame.runs.len() as u32;
650            for index in self.row_range(row) {
651                let run = &self.runs[index];
652                if run.len == 0 {
653                    continue;
654                }
655                let tag = self.run_tags.get(index).copied().unwrap_or(0);
656                let text = &self.text[run.range()];
657                let last_tag = frame.run_tags.last().copied().unwrap_or(0);
658                match frame.runs[start as usize..].last_mut() {
659                    Some(last) if last.style == run.style && last_tag == tag => {
660                        last.len += run.len;
661                        last.cells += run.cells;
662                        frame.text.push_str(text);
663                    }
664                    _ => frame.push(text, run.style, 0, tag),
665                }
666            }
667            frame.rows.push(start..frame.runs.len() as u32);
668        }
669        frame
670    }
671
672    /// The number of rows. A final line break does not start a new row.
673    pub fn height(&self) -> usize {
674        self.rows.len()
675    }
676
677    /// The runs of row `index`. Panics when `index >= height()`.
678    pub fn row(&self, index: usize) -> &[Run] {
679        &self.runs[self.row_range(index)]
680    }
681
682    /// The indices of row `index`'s runs.
683    fn row_range(&self, index: usize) -> Range<usize> {
684        let range = &self.rows[index];
685        range.start as usize..range.end as usize
686    }
687
688    /// The region tag of run `index`, if it has one.
689    fn run_tag(&self, index: usize) -> Option<RegionId> {
690        match self.run_tags.get(index).copied().unwrap_or(0) {
691            0 => None,
692            tag => self.tags.get(tag as usize - 1).copied(),
693        }
694    }
695
696    /// The width of row `index` in cells.
697    pub fn row_width(&self, index: usize) -> usize {
698        self.row(index).iter().map(Run::cells).sum()
699    }
700
701    /// The widest row, in cells.
702    pub fn width(&self) -> usize {
703        (0..self.height())
704            .map(|row| self.row_width(row))
705            .max()
706            .unwrap_or(0)
707    }
708
709    /// The text of `run`.
710    pub fn run_text(&self, run: &Run) -> &str {
711        &self.text[run.range()]
712    }
713
714    pub fn styles(&self) -> &StyleTable {
715        &self.styles
716    }
717
718    /// Whether the stream ended with a line break.
719    pub fn ends_with_newline(&self) -> bool {
720        self.trailing_newline
721    }
722
723    /// The total number of runs.
724    pub fn run_count(&self) -> usize {
725        self.runs.len()
726    }
727
728    /// Plain text: rows joined by line breaks, without styles.
729    pub fn plain(&self) -> String {
730        let mut out = String::with_capacity(self.text.len() + self.height());
731        for row in 0..self.height() {
732            if row > 0 {
733                out.push('\n');
734            }
735            for run in self.row(row) {
736                out.push_str(self.run_text(run));
737            }
738        }
739        if self.trailing_newline {
740            out.push('\n');
741        }
742        out
743    }
744
745    /// Encode as `console` would: the same bytes as
746    /// `console.segments_to_string` on the stream this frame was built from,
747    /// less its control segments.
748    pub fn to_ansi(&self, console: &Console) -> String {
749        self.encode(console.color_system(), console.no_color())
750    }
751
752    /// Encode with an explicit colour system. `no_color` removes colours but
753    /// keeps other attributes, as `Console` does when `no_color` is set.
754    pub fn encode(&self, system: Option<ColorSystem>, no_color: bool) -> String {
755        let styles = self.styles_for(system, no_color);
756        let mut out = String::with_capacity(self.text.len() * 2);
757        // A segment's pieces render as one: its line breaks sit inside the
758        // same SGR pair, as upstream's `_render_buffer` writes them.
759        let mut piece = String::new();
760        let mut style = StyleId::NONE;
761        for row in 0..self.height() {
762            let runs = self.row(row);
763            for run in runs {
764                if run.flags & JOIN == 0 {
765                    flush(&mut out, &piece, styles[style.index()].as_ref(), system);
766                    piece.clear();
767                    style = run.style;
768                }
769                piece.push_str(&self.text[run.range()]);
770                if run.flags & NEWLINE != 0 {
771                    piece.push('\n');
772                }
773            }
774            // A merged frame's line breaks belong to no run: write them bare.
775            let breaks = row + 1 < self.height() || self.trailing_newline;
776            if breaks && runs.last().is_none_or(|run| run.flags & NEWLINE == 0) {
777                flush(&mut out, &piece, styles[style.index()].as_ref(), system);
778                piece.clear();
779                out.push('\n');
780            }
781        }
782        flush(&mut out, &piece, styles[style.index()].as_ref(), system);
783        out
784    }
785
786    /// Encode after merging adjacent runs of one style, with line breaks
787    /// unstyled. Fewer bytes than [`Frame::to_ansi`], but not upstream's.
788    pub fn to_ansi_merged(&self, console: &Console) -> String {
789        self.merged()
790            .encode(console.color_system(), console.no_color())
791    }
792
793    /// Encode the runs covering `columns` of row `row` (clipped to the row),
794    /// merging adjacent runs of one style. A run that starts before the range
795    /// is cut at the grapheme boundary; a wide grapheme cut in half becomes a
796    /// space. This is what a cell-level repaint writes for one [`Change`].
797    pub fn encode_span(
798        &self,
799        row: usize,
800        columns: Range<usize>,
801        system: Option<ColorSystem>,
802        no_color: bool,
803    ) -> String {
804        let styles = self.styles_for(system, no_color);
805        let mut out = String::new();
806        let mut piece = String::new();
807        let mut style = None;
808        for cell in self
809            .cells(row)
810            .into_iter()
811            .skip(columns.start)
812            .take(columns.len())
813        {
814            if style != Some(cell.style) {
815                if let Some(id) = style {
816                    flush(
817                        &mut out,
818                        &piece,
819                        styles[StyleId::index(id)].as_ref(),
820                        system,
821                    );
822                }
823                piece.clear();
824                style = Some(cell.style);
825            }
826            if cell.is_continuation() {
827                if piece.is_empty() {
828                    piece.push(' ');
829                }
830                continue;
831            }
832            piece.push_str(cell.text);
833        }
834        if let Some(id) = style {
835            flush(&mut out, &piece, styles[id.index()].as_ref(), system);
836        }
837        out
838    }
839
840    fn styles_for(&self, system: Option<ColorSystem>, no_color: bool) -> Vec<Option<Style>> {
841        if no_color && system.is_some() {
842            self.styles
843                .styles
844                .iter()
845                .map(|style| style.as_ref().map(Style::without_color))
846                .collect()
847        } else {
848            self.styles.styles.clone()
849        }
850    }
851
852    /// One cell per grapheme of row `index`, using core's widths. A wide
853    /// grapheme is followed by continuation cells, so a cell's index in the
854    /// result is its column. A zero-width grapheme occupies no column: it
855    /// joins the cell before it, even across runs, or the first cell of the
856    /// row when nothing precedes it.
857    pub fn cells(&self, index: usize) -> Vec<Cell<'_>> {
858        let row = self.row(index);
859        // Arena ranges first: the runs of a row are stored in order in the
860        // text arena, so a zero-width grapheme extends its neighbour's range.
861        let mut cells: Vec<(Range<usize>, u8, StyleId)> =
862            Vec::with_capacity(row.iter().map(Run::cells).sum());
863        let mut leading: Option<usize> = None;
864        for run in row {
865            let base = run.text as usize;
866            let text = self.run_text(run);
867            if leading.is_none() && simple_text(text) {
868                // A character a grapheme: no splitting.
869                let mut at = base;
870                for c in text.chars() {
871                    let (len, width) = (c.len_utf8(), crate::surface::char_width(c));
872                    cells.push((at..at + len, width as u8, run.style));
873                    at += len;
874                    for _ in 1..width {
875                        cells.push((at..at, 0, run.style));
876                    }
877                }
878                continue;
879            }
880            let (graphemes, _) = split_graphemes(text);
881            for (start, end, width) in graphemes {
882                let range = base + start..base + end;
883                if width == 0 {
884                    match cells.iter_mut().rev().find(|cell| cell.1 > 0) {
885                        Some(lead) if lead.0.end == range.start => lead.0.end = range.end,
886                        Some(_) => {}
887                        None => {
888                            leading.get_or_insert(range.start);
889                        }
890                    }
891                    continue;
892                }
893                let start = leading.take().unwrap_or(range.start);
894                cells.push((start..range.end, width as u8, run.style));
895                for _ in 1..width {
896                    cells.push((range.end..range.end, 0, run.style));
897                }
898            }
899        }
900        cells
901            .into_iter()
902            .map(|(range, width, style)| Cell {
903                text: &self.text[range],
904                width,
905                style,
906            })
907            .collect()
908    }
909
910    /// The cells that differ from `previous`, as column ranges per row, in
911    /// row order. Styles compare by value, so the two frames need not share a
912    /// style table. A row present in only one frame changes across its width.
913    pub fn diff(&self, previous: &Frame) -> Vec<Change> {
914        let mut changes = Vec::new();
915        for row in 0..self.height().max(previous.height()) {
916            let new = if row < self.height() {
917                self.cells(row)
918            } else {
919                Vec::new()
920            };
921            let old = if row < previous.height() {
922                previous.cells(row)
923            } else {
924                Vec::new()
925            };
926            let width = new.len().max(old.len());
927            let mut open = None;
928            for column in 0..width {
929                let same = match (new.get(column), old.get(column)) {
930                    (Some(a), Some(b)) => {
931                        a.text == b.text
932                            && a.width == b.width
933                            && self.styles.get(a.style) == previous.styles.get(b.style)
934                    }
935                    _ => false,
936                };
937                match (same, open) {
938                    (false, None) => open = Some(column),
939                    (true, Some(start)) => {
940                        changes.push(Change {
941                            row,
942                            columns: start..column,
943                        });
944                        open = None;
945                    }
946                    _ => {}
947                }
948            }
949            if let Some(start) = open {
950                changes.push(Change {
951                    row,
952                    columns: start..width,
953                });
954            }
955        }
956        changes
957    }
958}
959
960fn flush(out: &mut String, text: &str, style: Option<&Style>, system: Option<ColorSystem>) {
961    match (style, system) {
962        (Some(style), Some(system)) => out.push_str(&style.render(text, Some(system))),
963        _ => out.push_str(text),
964    }
965}
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970
971    fn seg(text: &str, style: Option<&str>) -> Segment {
972        Segment::new(text, style.map(|s| Style::parse(s).unwrap()))
973    }
974
975    fn console(system: Option<ColorSystem>, no_color: bool) -> Console {
976        Console::builder()
977            .width(40)
978            .force_terminal(true)
979            .color_system(system)
980            .no_color(no_color)
981            .build()
982    }
983
984    #[test]
985    fn exact_encoding_keeps_line_breaks_inside_a_segment() {
986        let segments = vec![
987            seg("a\nb", Some("bold")),
988            seg("c", None),
989            seg("\n", None),
990            seg("d\n", Some("red link https://x.test")),
991        ];
992        let console = console(Some(ColorSystem::Truecolor), false);
993        let frame = Frame::from_segments(&segments);
994        assert_eq!(frame.height(), 3);
995        assert!(frame.ends_with_newline());
996        assert_eq!(frame.plain(), "a\nbc\nd\n");
997        assert_eq!(
998            frame.to_ansi(&console),
999            console.segments_to_string(&segments)
1000        );
1001    }
1002
1003    #[test]
1004    fn control_segments_are_dropped() {
1005        let segments = vec![seg("x", None), Segment::control("\x1b[2K"), seg("y", None)];
1006        let frame = Frame::from_segments(&segments);
1007        assert_eq!(frame.plain(), "xy");
1008        assert_eq!(frame.run_count(), 2);
1009    }
1010
1011    #[test]
1012    fn styles_are_interned_once() {
1013        let segments = vec![
1014            seg("a", Some("bold")),
1015            seg("b", Some("italic")),
1016            seg("c", Some("bold")),
1017            seg("d", None),
1018        ];
1019        let frame = Frame::from_segments(&segments);
1020        assert_eq!(frame.styles().len(), 3);
1021        let row = frame.row(0);
1022        assert_eq!(row[0].style(), row[2].style());
1023        assert_eq!(row[3].style(), StyleId::NONE);
1024    }
1025
1026    #[test]
1027    fn no_color_keeps_attributes() {
1028        let segments = vec![seg("hot", Some("bold red on blue"))];
1029        let console = console(Some(ColorSystem::Standard), true);
1030        let frame = Frame::from_segments(&segments);
1031        assert_eq!(
1032            frame.to_ansi(&console),
1033            console.segments_to_string(&segments)
1034        );
1035        assert_eq!(frame.to_ansi(&console), "\x1b[1mhot\x1b[0m");
1036    }
1037
1038    #[test]
1039    fn merging_joins_runs_of_one_style() {
1040        let segments = vec![
1041            seg("ab", Some("bold")),
1042            seg("cd", Some("bold")),
1043            seg("", Some("red")),
1044            seg("\n", None),
1045            seg("e", None),
1046        ];
1047        let console = console(Some(ColorSystem::Truecolor), false);
1048        let frame = Frame::from_segments(&segments);
1049        let merged = frame.merged();
1050        assert_eq!(merged.row(0).len(), 1);
1051        assert_eq!(frame.to_ansi_merged(&console), "\x1b[1mabcd\x1b[0m\ne");
1052        assert_eq!(merged.plain(), frame.plain());
1053    }
1054
1055    #[test]
1056    fn cells_follow_core_widths() {
1057        let frame = Frame::from_segments(&[seg("a漢b", None)]);
1058        let cells = frame.cells(0);
1059        let widths: Vec<u8> = cells.iter().map(|cell| cell.width).collect();
1060        assert_eq!(widths, [1, 2, 0, 1]);
1061        assert!(cells[2].is_continuation());
1062        assert_eq!(frame.width(), 4);
1063    }
1064
1065    #[test]
1066    fn zero_width_graphemes_take_no_column() {
1067        // A combining mark styled on its own joins the cell before it.
1068        let frame =
1069            Frame::from_segments(&[seg("a", None), seg("\u{301}", Some("bold")), seg("x", None)]);
1070        let cells = frame.cells(0);
1071        assert_eq!(cells.len(), 2);
1072        assert_eq!(cells[0].text, "a\u{301}");
1073        assert_eq!(cells[1].text, "x");
1074        let changed =
1075            Frame::from_segments(&[seg("a", None), seg("\u{301}", Some("bold")), seg("y", None)]);
1076        assert_eq!(
1077            changed.diff(&frame),
1078            [Change {
1079                row: 0,
1080                columns: 1..2
1081            }]
1082        );
1083        // After a wide grapheme it joins the lead, not the continuation.
1084        let wide = Frame::from_segments(&[seg("漢", None), seg("\u{301}", None), seg("z", None)]);
1085        let cells = wide.cells(0);
1086        assert_eq!(cells.len(), 3);
1087        assert_eq!(cells[0].text, "漢\u{301}");
1088        assert!(cells[1].is_continuation());
1089        // At the start of a row it joins the first cell.
1090        let leading = Frame::from_segments(&[seg("\u{301}", None), seg("b", None)]);
1091        let cells = leading.cells(0);
1092        assert_eq!(cells.len(), 1);
1093        assert_eq!(cells[0].text, "\u{301}b");
1094    }
1095
1096    #[test]
1097    fn diff_reports_changed_cells() {
1098        let before = Frame::from_segments(&[seg("hello\n", None), seg("world", None)]);
1099        let after = Frame::from_segments(&[
1100            seg("hel", None),
1101            seg("l", Some("bold")),
1102            seg("o\n", None),
1103            seg("world!", None),
1104            seg("\nnew", None),
1105        ]);
1106        assert_eq!(
1107            after.diff(&before),
1108            [
1109                Change {
1110                    row: 0,
1111                    columns: 3..4
1112                },
1113                Change {
1114                    row: 1,
1115                    columns: 5..6
1116                },
1117                Change {
1118                    row: 2,
1119                    columns: 0..3
1120                },
1121            ]
1122        );
1123        assert!(before.diff(&before).is_empty());
1124        // Resegmented but identical output has no changes.
1125        let split = Frame::from_segments(&[seg("hel", None), seg("lo\nworld", None)]);
1126        assert!(split.diff(&before).is_empty());
1127    }
1128
1129    #[test]
1130    fn encode_span_repaints_one_change() {
1131        let frame = Frame::from_segments(&[seg("ab", None), seg("cd", Some("bold"))]);
1132        let span = frame.encode_span(0, 1..3, Some(ColorSystem::Standard), false);
1133        assert_eq!(span, "b\x1b[1mc\x1b[0m");
1134        let wide = Frame::from_segments(&[seg("漢x", None)]);
1135        assert_eq!(wide.encode_span(0, 1..3, None, false), " x");
1136    }
1137
1138    #[test]
1139    fn styles_that_hash_alike_stay_apart() {
1140        // Metadata is not in the key: these two share one, and a chain.
1141        let plain = Style::parse("bold").unwrap();
1142        let meta: rich::style::Meta = [("k", rich::style::MetaValue::Int(1))]
1143            .into_iter()
1144            .collect();
1145        let tagged = plain.clone().with_meta(meta);
1146        let mut table = StyleTable::new();
1147        let a = table.intern(Some(&plain));
1148        let b = table.intern(Some(&tagged));
1149        assert_ne!(a, b);
1150        assert_eq!(table.intern(Some(&plain)), a);
1151        assert_eq!(table.intern(Some(&tagged)), b);
1152        let remap = table.retain(&[true, false, true]);
1153        let b = remap.get(b).unwrap();
1154        assert_eq!(table.get(b), Some(&tagged));
1155        assert_eq!(table.intern(Some(&tagged)), b);
1156        assert_eq!(table.intern(Some(&plain)), StyleId::from_index(2));
1157    }
1158
1159    #[test]
1160    fn colour_pairs_and_styles_share_ids() {
1161        let mut table = StyleTable::new();
1162        let style = Style::from_color(None, Some(rich::Color::from_rgb(1, 2, 3)));
1163        let id = table.intern(Some(&style));
1164        // A style of true colours alone is known as a pair at once.
1165        assert_eq!(table.rgb(id), Some((None, Some([1, 2, 3]))));
1166        assert_eq!(table.intern_rgb(None, Some([1, 2, 3])), id);
1167        assert_eq!(table.rgb(id), Some((None, Some([1, 2, 3]))));
1168        assert_eq!(table.intern_rgb(None, None), StyleId::NONE);
1169        let other = table.intern_rgb(Some([0, 0, 0]), Some([1, 2, 3]));
1170        assert_ne!(other, id);
1171        // Kept through a compaction, found again by its colours.
1172        let remap = table.retain(&[true, false, true]);
1173        assert_eq!(remap.get(id), None);
1174        let other = remap.get(other).unwrap();
1175        assert_eq!(table.intern_rgb(Some([0, 0, 0]), Some([1, 2, 3])), other);
1176        assert!(!table.piled_up());
1177    }
1178}