Skip to main content

standard_plugin/surface/
cells.rs

1//! The cell model: one grapheme, two colours and attributes per cell.
2//!
3//! A cell stores one Unicode scalar value, the grapheme's first. Wide
4//! characters take two cells: the character in the left one and an empty
5//! "spacer" to its right, as the viewer lays them out. The viewer's rule
6//! decides a character's width (its scalar's East Asian width: 1 or 2);
7//! control, bidi-control and zero-width characters never reach a cell.
8//! Combining marks, variation selectors and joined sequences are dropped
9//! with the rest of the grapheme (the grapheme pool that would carry them is
10//! reserved in the contract, not implemented).
11
12use unicode_segmentation::UnicodeSegmentation as _;
13use unicode_width::UnicodeWidthChar as _;
14
15use super::{Cells, Inner, Surface};
16use crate::colour::{Attrs, Colour, Style};
17use crate::geometry::Rect;
18
19const CELL_WORDS: usize = 4;
20
21/// One cell, read back from the buffer.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub struct Cell {
24    /// `None` paints as empty (a space in the cell's background); the right
25    /// half of a wide character is empty too.
26    pub grapheme: Option<char>,
27    pub style: Style,
28}
29
30/// Mirrors the viewer: C0, DEL and C1 controls, and the bidi controls a
31/// cell must never carry.
32fn is_forbidden(character: char) -> bool {
33    character.is_control()
34        || matches!(
35            character,
36            '\u{061c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}'
37        )
38}
39
40/// The columns a character takes in a cell, as the viewer decides: 1 or 2,
41/// or 0 for what a cell never shows.
42pub(crate) fn char_width(character: char) -> u32 {
43    if is_forbidden(character) {
44        return 0;
45    }
46    match character.width() {
47        Some(1) => 1,
48        Some(2) => 2,
49        _ => 0,
50    }
51}
52
53fn word_width(word: u32) -> u32 {
54    if word == 0 || word >= 0x8000_0000 {
55        return 0;
56    }
57    char::from_u32(word).map_or(0, char_width)
58}
59
60struct Grid<'a> {
61    slot: &'a mut [u32],
62    cols: u32,
63    rows: u32,
64}
65
66impl Grid<'_> {
67    fn index(&self, x: u32, y: u32) -> usize {
68        (y as usize * self.cols as usize + x as usize) * CELL_WORDS
69    }
70
71    fn grapheme(&self, x: u32, y: u32) -> u32 {
72        self.slot[self.index(x, y)]
73    }
74
75    fn write(&mut self, x: u32, y: u32, grapheme: u32, style: Style) {
76        let index = self.index(x, y);
77        self.slot[index..index + CELL_WORDS].copy_from_slice(&[
78            grapheme,
79            style.fg.encode(),
80            style.bg.encode(),
81            u32::from(style.attrs.0),
82        ]);
83    }
84
85    /// Writes `character` of `width` columns at `(x, y)` (in bounds, the
86    /// right half too) and returns the rectangle the viewer must re-read:
87    /// the cells written, and neighbours whose wide character the write
88    /// split.
89    fn put(&mut self, x: u32, y: u32, character: u32, width: u32, style: Style) -> Rect {
90        let mut left = x;
91        let mut right = x + width;
92        // Overwriting the right half of a wide character: its left half
93        // would still claim this cell, so it becomes empty.
94        if x > 0 && word_width(self.grapheme(x - 1, y)) == 2 {
95            let index = self.index(x - 1, y);
96            self.slot[index] = 0;
97            left = x - 1;
98        }
99        // Overwriting the left half of a wide character whose right half
100        // this write does not cover: that spacer must be re-read as empty.
101        let last = x + width - 1;
102        if word_width(self.grapheme(last, y)) == 2 && last + 1 < self.cols {
103            right = last + 2;
104        }
105        self.write(x, y, character, style);
106        if width == 2 {
107            self.write(x + 1, y, 0, style);
108        }
109        Rect::new(left, y, right - left, 1)
110    }
111}
112
113impl Inner {
114    fn grid(&mut self) -> Option<(Grid<'_>, &mut super::DirtyRects)> {
115        let (slot, layout, dirty) = self.back()?;
116        let g = layout.geometry;
117        Some((
118            Grid {
119                slot,
120                cols: g.cols,
121                rows: g.rows,
122            },
123            dirty,
124        ))
125    }
126}
127
128impl Surface<Cells> {
129    /// Writes one character at `(x, y)`. Returns the columns it took: 1,
130    /// 2 for a wide character, 0 when nothing was written (out of bounds,
131    /// or a character a cell never shows). A wide character that does not
132    /// fit before the right edge leaves one empty cell instead (the
133    /// viewer would drop it).
134    pub fn put(&mut self, x: u32, y: u32, character: char, style: Style) -> u32 {
135        let mut inner = self.inner.borrow_mut();
136        let Some((mut grid, dirty)) = inner.grid() else {
137            return 0;
138        };
139        put_char(&mut grid, dirty, x, y, character, style)
140    }
141
142    /// Writes `text` from `(x, y)` along one row, grapheme by grapheme,
143    /// clipped at the right edge. Returns the columns written. Each
144    /// grapheme shows as its first character (see the module docs).
145    pub fn text(&mut self, x: u32, y: u32, text: &str, style: Style) -> u32 {
146        write_text_clipped(self, i64::from(x), i64::from(y), text, style)
147    }
148
149    /// Fills `rect` with `character` in `style` (clipped).
150    pub fn fill(&mut self, rect: Rect, character: char, style: Style) {
151        let mut inner = self.inner.borrow_mut();
152        let Some((mut grid, dirty)) = inner.grid() else {
153            return;
154        };
155        let Some(rect) = rect.clamp(grid.cols, grid.rows) else {
156            return;
157        };
158        let step = char_width(character).max(1);
159        for y in rect.y..rect.bottom() {
160            let mut x = rect.x;
161            while x < rect.right() {
162                if x + step > rect.right() {
163                    put_char(&mut grid, dirty, x, y, ' ', style);
164                    break;
165                }
166                put_char(&mut grid, dirty, x, y, character, style);
167                x += step;
168            }
169        }
170    }
171
172    /// Empties every cell (theme text on a transparent background). The
173    /// whole surface is dirty; the previous frame is not copied forward.
174    pub fn clear(&mut self) {
175        self.clear_with(Style::DEFAULT);
176    }
177
178    /// Empties every cell with `style` (its background shows).
179    pub fn clear_with(&mut self, style: Style) {
180        let mut inner = self.inner.borrow_mut();
181        let Some((slot, _)) = inner.back_discard() else {
182            return;
183        };
184        let words = [
185            0,
186            style.fg.encode(),
187            style.bg.encode(),
188            u32::from(style.attrs.0),
189        ];
190        for cell in slot.chunks_exact_mut(CELL_WORDS) {
191            cell.copy_from_slice(&words);
192        }
193    }
194
195    /// The cell at `(x, y)` as it will be committed next.
196    pub fn cell(&self, x: u32, y: u32) -> Option<Cell> {
197        let mut inner = self.inner.borrow_mut();
198        let (grid, _) = inner.grid()?;
199        if x >= grid.cols || y >= grid.rows {
200            return None;
201        }
202        let index = grid.index(x, y);
203        let words = &grid.slot[index..index + CELL_WORDS];
204        Some(Cell {
205            grapheme: (word_width(words[0]) > 0)
206                .then(|| char::from_u32(words[0]))
207                .flatten(),
208            style: Style {
209                fg: Colour::decode(words[1]),
210                bg: Colour::decode(words[2]),
211                attrs: Attrs(words[3] as u16),
212            },
213        })
214    }
215
216    /// One row as text: each cell's character, a space for an empty cell,
217    /// nothing for the right half of a wide character. For tests.
218    pub fn row_text(&self, y: u32) -> alloc::string::String {
219        let (cols, _) = self.size();
220        let mut text = alloc::string::String::new();
221        let mut x = 0;
222        while x < cols {
223            let Some(cell) = self.cell(x, y) else {
224                break;
225            };
226            match cell.grapheme {
227                Some(character) => {
228                    text.push(character);
229                    x += char_width(character);
230                }
231                None => {
232                    text.push(' ');
233                    x += 1;
234                }
235            }
236        }
237        text
238    }
239}
240
241fn put_char(
242    grid: &mut Grid<'_>,
243    dirty: &mut super::DirtyRects,
244    x: u32,
245    y: u32,
246    character: char,
247    style: Style,
248) -> u32 {
249    if x >= grid.cols || y >= grid.rows {
250        return 0;
251    }
252    let width = char_width(character);
253    if width == 0 {
254        return 0;
255    }
256    let (word, width) = if x + width > grid.cols {
257        (0, 1)
258    } else {
259        (character as u32, width)
260    };
261    let rect = grid.put(x, y, word, width, style);
262    dirty.add(rect, grid.cols, grid.rows);
263    width
264}
265
266/// Writes `text` from a column that may lie left of the surface; what
267/// falls outside is clipped. Returns the columns written.
268pub(crate) fn write_text_clipped(
269    surface: &mut Surface<Cells>,
270    x: i64,
271    y: i64,
272    text: &str,
273    style: Style,
274) -> u32 {
275    let mut inner = surface.inner.borrow_mut();
276    let Some((mut grid, dirty)) = inner.grid() else {
277        return 0;
278    };
279    if y < 0 || y >= i64::from(grid.rows) {
280        return 0;
281    }
282    let mut column = x;
283    let mut written = 0;
284    for grapheme in text.graphemes(true) {
285        let Some(character) = grapheme.chars().next() else {
286            continue;
287        };
288        let width = char_width(character);
289        if width == 0 {
290            continue;
291        }
292        if column >= i64::from(grid.cols) {
293            break;
294        }
295        if column >= 0 {
296            written += put_char(&mut grid, dirty, column as u32, y as u32, character, style);
297        } else if column + i64::from(width) > 0 {
298            // A wide character straddling the left edge: its visible half
299            // is an empty cell.
300            written += put_char(&mut grid, dirty, 0, y as u32, ' ', style);
301        }
302        column += i64::from(width);
303    }
304    written
305}