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}