Skip to main content

par_term_config/
cell.rs

1/// A single terminal cell with styled content for rendering.
2///
3/// This is the bridge between terminal emulation (core library cells with VT attributes)
4/// and GPU rendering (colored rectangles and textured glyphs). The `TerminalManager`
5/// converts core cells into these, applying theme colors and selection state.
6#[derive(Debug, PartialEq)]
7pub struct Cell {
8    /// The grapheme cluster displayed in this cell (typically one character or a composed sequence).
9    pub grapheme: String,
10    /// Foreground color as RGBA (0–255 per channel).
11    pub fg_color: [u8; 4],
12    /// Background color as RGBA (0–255 per channel). Alpha 0 means transparent (default background).
13    pub bg_color: [u8; 4],
14    /// Whether to render the cell's font in bold weight.
15    pub bold: bool,
16    /// Whether to render the cell's font in italic style.
17    pub italic: bool,
18    /// Whether to draw an underline below the cell's glyph.
19    pub underline: bool,
20    /// Whether to draw a strikethrough line through the cell's glyph.
21    pub strikethrough: bool,
22    /// Optional OSC 8 hyperlink ID. Non-None cells are clickable and open a URL.
23    pub hyperlink_id: Option<u32>,
24    /// True if this cell holds the left half of a wide (double-width) character.
25    pub wide_char: bool,
26    /// True if this cell is the right-half spacer of a wide character. Has no renderable content.
27    pub wide_char_spacer: bool,
28}
29
30/// Hand-written so `clone_from` reuses the destination's `grapheme` allocation.
31///
32/// `#[derive(Clone)]` does not emit a `clone_from` override, so the default
33/// trait method (`*self = source.clone()`) runs and every cell allocates a new
34/// `String`. The render path refreshes a persistent full-grid scratch buffer
35/// once per frame via `Vec::clone_from`, which dispatches to this method per
36/// cell — with the derive that was one heap allocation per cell per frame.
37impl Clone for Cell {
38    fn clone(&self) -> Self {
39        Self {
40            grapheme: self.grapheme.clone(),
41            fg_color: self.fg_color,
42            bg_color: self.bg_color,
43            bold: self.bold,
44            italic: self.italic,
45            underline: self.underline,
46            strikethrough: self.strikethrough,
47            hyperlink_id: self.hyperlink_id,
48            wide_char: self.wide_char,
49            wide_char_spacer: self.wide_char_spacer,
50        }
51    }
52
53    fn clone_from(&mut self, source: &Self) {
54        self.grapheme.clone_from(&source.grapheme);
55        self.fg_color = source.fg_color;
56        self.bg_color = source.bg_color;
57        self.bold = source.bold;
58        self.italic = source.italic;
59        self.underline = source.underline;
60        self.strikethrough = source.strikethrough;
61        self.hyperlink_id = source.hyperlink_id;
62        self.wide_char = source.wide_char;
63        self.wide_char_spacer = source.wide_char_spacer;
64    }
65}
66
67impl Default for Cell {
68    fn default() -> Self {
69        Self {
70            grapheme: " ".to_string(),
71            fg_color: [255, 255, 255, 255],
72            bg_color: [0, 0, 0, 0],
73            bold: false,
74            italic: false,
75            underline: false,
76            strikethrough: false,
77            hyperlink_id: None,
78            wide_char: false,
79            wide_char_spacer: false,
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::Cell;
87
88    fn cell(grapheme: &str) -> Cell {
89        Cell {
90            grapheme: grapheme.to_string(),
91            ..Cell::default()
92        }
93    }
94
95    /// Guards the reason `Clone` is hand-written instead of derived: the render
96    /// path's per-frame scratch refresh relies on `clone_from` reusing the
97    /// destination's `String` buffer. Reverting to `#[derive(Clone)]` silently
98    /// reintroduces one heap allocation per cell per frame, and this fails.
99    #[test]
100    fn clone_from_reuses_the_destination_grapheme_allocation() {
101        // Destination capacity must already cover the source, or `String::clone_from`
102        // legitimately reallocates.
103        let mut dst = cell("placeholder");
104        let before = dst.grapheme.as_ptr();
105
106        dst.clone_from(&cell("x"));
107
108        assert_eq!(dst.grapheme, "x");
109        assert_eq!(
110            dst.grapheme.as_ptr(),
111            before,
112            "clone_from must reuse the existing grapheme buffer"
113        );
114    }
115
116    #[test]
117    fn clone_from_copies_every_field() {
118        let source = Cell {
119            grapheme: "@".to_string(),
120            fg_color: [1, 2, 3, 4],
121            bg_color: [5, 6, 7, 8],
122            bold: true,
123            italic: true,
124            underline: true,
125            strikethrough: true,
126            hyperlink_id: Some(9),
127            wide_char: true,
128            wide_char_spacer: true,
129        };
130        let mut dst = Cell::default();
131
132        dst.clone_from(&source);
133
134        assert_eq!(dst, source);
135        assert_eq!(dst, source.clone());
136    }
137}