Skip to main content

rich_ext/frame/
graphics.rs

1//! The graphics side channel: images placed on a frame's cells.
2//!
3//! A [`Frame`] holds text; control segments are dropped. Terminal graphics
4//! (Kitty, iTerm2 and Sixel images) travel beside the cells instead, as
5//! [`Placement`]s: a row, a column, a size in cells, the [`Graphic`] and
6//! which of its frames shows. Whatever paints the frame writes the cell
7//! diff first, then works out from [`plan`] what graphics to redraw:
8//!
9//! - an overlay (iTerm2, Sixel) is redrawn when it is new, moved, on another
10//!   animation frame, or when a cell under it was repainted (which wiped
11//!   it);
12//! - an overlay that went away has its cells repainted, which wipes it;
13//! - a graphic that lives in its cells (Kitty's Unicode placeholders) goes
14//!   and comes with those cells, so it is only ever *drawn* to make sure the
15//!   terminal holds the image.
16//!
17//! The cells under a placement carry the layout (a micro asset's
18//! placeholder cells), so the cell diff stays exact whatever the graphics
19//! do, and a painter without graphics shows those cells and nothing leaks.
20//!
21//! A [`PlacementSource`] finds the placements in a frame's cells (for micro
22//! assets, the cells tagged `rich.micro`), may swap cells for others of the
23//! same width before the frame is built ([`PlacementSource::prepare`]), and
24//! releases what the terminal holds when the view closes.
25
26use std::fmt;
27use std::ops::Range;
28use std::sync::Arc;
29use std::time::Duration;
30
31use rich::Segment;
32
33use super::{Change, Frame};
34
35/// Something a terminal graphics protocol draws on a frame's cells.
36pub trait Graphic: Send + Sync + fmt::Debug {
37    /// What is drawn: two graphics with one key draw the same pixels.
38    fn key(&self) -> u64;
39
40    /// The escape that draws animation frame `frame` with the cursor on the
41    /// placement's top-left cell. It must leave the cursor where it found it
42    /// (save and restore it around anything that moves it).
43    fn draw(&self, frame: usize) -> String;
44
45    /// Whether the image lives in the cells themselves (Kitty's Unicode
46    /// placeholders): repainting the cells repaints it, and [`draw`](Self::draw)
47    /// only makes sure the terminal holds the image.
48    fn in_cells(&self) -> bool {
49        false
50    }
51}
52
53/// A graphic placed on a frame: its top-left cell, its size in cells, and
54/// the animation frame that shows.
55#[derive(Clone, Debug)]
56pub struct Placement {
57    pub row: usize,
58    pub column: usize,
59    pub cols: usize,
60    pub rows: usize,
61    pub graphic: Arc<dyn Graphic>,
62    pub frame: usize,
63}
64
65impl Placement {
66    /// Whether `self` and `other` show the same thing in the same place.
67    pub fn same(&self, other: &Placement) -> bool {
68        self.row == other.row
69            && self.column == other.column
70            && self.cols == other.cols
71            && self.rows == other.rows
72            && self.frame == other.frame
73            && self.graphic.key() == other.graphic.key()
74    }
75
76    /// The columns it covers.
77    pub fn columns(&self) -> Range<usize> {
78        self.column..self.column + self.cols
79    }
80
81    /// Whether `change` repainted any of its cells.
82    pub fn touched_by(&self, change: &Change) -> bool {
83        (self.row..self.row + self.rows).contains(&change.row)
84            && change.columns.start < self.column + self.cols
85            && self.column < change.columns.end
86    }
87}
88
89/// Finds the graphics placed on a frame, and speaks for them to painters.
90pub trait PlacementSource: Send + Sync {
91    /// Swap cells of one painted line for others of the same width before
92    /// the frame is built (for example, a micro asset's fallback cells for
93    /// Kitty placeholder cells). The default keeps the line.
94    fn prepare(&self, line: Vec<Segment>) -> Vec<Segment> {
95        line
96    }
97
98    /// The placements on `frame`'s cells.
99    fn placements(&self, frame: &Frame) -> Vec<Placement>;
100
101    /// How long until an animation among the placements shows another frame,
102    /// so an event loop knows when to repaint. `None` when nothing moves.
103    fn next_change(&self) -> Option<Duration> {
104        None
105    }
106
107    /// The escape that releases what the terminal holds for graphics this
108    /// source placed, once the view closes, except those in `retained` (still
109    /// on screen). Kitty images are deleted by id here.
110    fn release(&self, retained: &[Placement]) -> String {
111        let _ = retained;
112        String::new()
113    }
114}
115
116/// What a painter writes for graphics after the cell diff.
117#[derive(Clone, Debug, Default)]
118pub struct GraphicsPlan {
119    /// Cells to repaint because the overlay drawn on them went away:
120    /// `(row, columns)`.
121    pub repaint: Vec<(usize, Range<usize>)>,
122    /// Placements to draw, in order.
123    pub draw: Vec<Placement>,
124}
125
126impl GraphicsPlan {
127    pub fn is_empty(&self) -> bool {
128        self.repaint.is_empty() && self.draw.is_empty()
129    }
130}
131
132/// What to redraw going from `previous` placements to `next`, after the
133/// cell `changes` were painted.
134pub fn plan(previous: &[Placement], next: &[Placement], changes: &[Change]) -> GraphicsPlan {
135    let mut out = GraphicsPlan::default();
136    for old in previous {
137        if old.graphic.in_cells() || next.iter().any(|new| new.same(old)) {
138            continue;
139        }
140        // Repaint what the cell diff did not already.
141        for row in old.row..old.row + old.rows {
142            let mut open = None;
143            for column in old.columns() {
144                let painted = changes
145                    .iter()
146                    .any(|change| change.row == row && change.columns.contains(&column));
147                match (painted, open) {
148                    (false, None) => open = Some(column),
149                    (true, Some(start)) => {
150                        out.repaint.push((row, start..column));
151                        open = None;
152                    }
153                    _ => {}
154                }
155            }
156            if let Some(start) = open {
157                out.repaint.push((row, start..old.column + old.cols));
158            }
159        }
160    }
161    for new in next {
162        let kept = previous.iter().any(|old| old.same(new));
163        let wiped = !new.graphic.in_cells()
164            && (changes.iter().any(|change| new.touched_by(change))
165                || out.repaint.iter().any(|(row, columns)| {
166                    (new.row..new.row + new.rows).contains(row)
167                        && columns.start < new.column + new.cols
168                        && new.column < columns.end
169                }));
170        if !kept || wiped {
171            out.draw.push(new.clone());
172        }
173    }
174    out
175}
176
177impl Frame {
178    /// This frame with `placements` beside its cells.
179    pub fn with_placements(mut self, placements: Vec<Placement>) -> Frame {
180        self.placements = placements;
181        self
182    }
183
184    /// Replace the placements beside this frame's cells.
185    pub fn set_placements(&mut self, placements: Vec<Placement>) {
186        self.placements = placements;
187    }
188
189    /// The graphics placed on this frame's cells.
190    pub fn placements(&self) -> &[Placement] {
191        &self.placements
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[derive(Debug)]
200    struct Image(u64, bool);
201
202    impl Graphic for Image {
203        fn key(&self) -> u64 {
204            self.0
205        }
206        fn draw(&self, frame: usize) -> String {
207            format!("<img{}:{frame}>", self.0)
208        }
209        fn in_cells(&self) -> bool {
210            self.1
211        }
212    }
213
214    fn at(row: usize, column: usize, key: u64, frame: usize) -> Placement {
215        Placement {
216            row,
217            column,
218            cols: 2,
219            rows: 1,
220            graphic: Arc::new(Image(key, false)),
221            frame,
222        }
223    }
224
225    fn change(row: usize, columns: Range<usize>) -> Change {
226        Change { row, columns }
227    }
228
229    #[test]
230    fn unchanged_placements_draw_nothing() {
231        let placed = [at(0, 4, 1, 0)];
232        assert!(plan(&placed, &placed, &[]).is_empty());
233        // A change elsewhere leaves it.
234        assert!(plan(&placed, &placed, &[change(0, 0..2)]).is_empty());
235    }
236
237    #[test]
238    fn new_moved_animated_and_wiped_placements_are_drawn() {
239        let old = [at(0, 4, 1, 0)];
240        assert_eq!(plan(&[], &old, &[]).draw.len(), 1);
241        let moved = plan(&old, &[at(0, 6, 1, 0)], &[]);
242        assert_eq!(moved.draw.len(), 1);
243        // Its old cells 4..6 are repainted; 6..8 are where it is now.
244        assert_eq!(moved.repaint, vec![(0, 4..6)]);
245        assert_eq!(plan(&old, &[at(0, 4, 1, 1)], &[]).draw.len(), 1);
246        let wiped = plan(&old, &old, &[change(0, 5..9)]);
247        assert_eq!(wiped.draw.len(), 1);
248        assert!(wiped.repaint.is_empty());
249    }
250
251    #[test]
252    fn a_removed_overlay_repaints_only_cells_the_diff_did_not() {
253        let old = [at(1, 4, 1, 0)];
254        let out = plan(&old, &[], &[change(1, 3..5)]);
255        assert_eq!(out.repaint, vec![(1, 5..6)]);
256        assert!(out.draw.is_empty());
257    }
258
259    #[test]
260    fn graphics_in_cells_are_drawn_once_and_never_repainted() {
261        let kitty = Placement {
262            graphic: Arc::new(Image(9, true)),
263            ..at(0, 0, 9, 0)
264        };
265        assert_eq!(plan(&[], std::slice::from_ref(&kitty), &[]).draw.len(), 1);
266        let again = plan(
267            std::slice::from_ref(&kitty),
268            std::slice::from_ref(&kitty),
269            &[change(0, 0..2)],
270        );
271        assert!(again.is_empty());
272        assert!(plan(std::slice::from_ref(&kitty), &[], &[]).is_empty());
273    }
274}