Skip to main content

freya_terminal/
rendering.rs

1use std::hash::{
2    Hash,
3    Hasher,
4};
5
6use alacritty_terminal::{
7    selection::SelectionRange,
8    term::cell::{
9        Cell,
10        Flags,
11    },
12};
13use freya_core::{
14    fifo_cache::FifoCache,
15    prelude::Color,
16};
17use freya_engine::prelude::{
18    Canvas,
19    Font,
20    FontCollection,
21    Paint,
22    Paragraph,
23    ParagraphBuilder,
24    ParagraphStyle,
25    SkRect,
26    TextBlob,
27    TextStyle,
28};
29use rustc_hash::FxHasher;
30use torin::prelude::Area;
31
32use crate::colors::map_ansi_color;
33
34/// Cached per-row drawing primitives keyed by a hash of the row contents.
35pub(crate) enum CachedRow {
36    /// Same-color glyph runs ready to be redrawn directly.
37    TextBlobs(Vec<(TextBlob, Color)>),
38    /// Skia paragraph used when font fallback was required (emoji, wide chars).
39    Paragraph(Paragraph),
40}
41
42/// Single renderer that draws the terminal background, a row's cell
43/// backgrounds, glyph runs, selection overlay, cursor, and scrollbar.
44pub(crate) struct Renderer<'a> {
45    pub canvas: &'a Canvas,
46    pub paint: &'a mut Paint,
47    pub font: &'a Font,
48    pub font_collection: &'a mut FontCollection,
49    pub row_cache: &'a mut FifoCache<u64, CachedRow>,
50    pub area: Area,
51    pub char_width: f32,
52    pub line_height: f32,
53    pub baseline_offset: f32,
54    pub foreground: Color,
55    pub background: Color,
56    pub selection_color: Color,
57    pub font_family: &'a str,
58    pub font_size: f32,
59    pub selection: Option<SelectionRange>,
60    pub display_offset: usize,
61}
62
63impl Renderer<'_> {
64    pub fn render_background(&mut self) {
65        self.paint.set_color(self.background);
66        self.canvas.draw_rect(
67            SkRect::new(
68                self.area.min_x(),
69                self.area.min_y(),
70                self.area.max_x(),
71                self.area.max_y(),
72            ),
73            self.paint,
74        );
75    }
76
77    /// Render one row: cell backgrounds, glyphs, then any selection overlay.
78    pub fn render_row(&mut self, row_idx: usize, row: &[Cell], y: f32) {
79        self.render_cell_backgrounds(row, y);
80        self.render_text_row(row, y);
81        self.render_selection(row_idx, row.len(), y);
82    }
83
84    pub fn render_cursor(&mut self, cell: &Cell, y: f32, cursor_col: usize) {
85        let left = self.area.min_x() + (cursor_col as f32) * self.char_width;
86        let right = left + self.char_width.max(1.0);
87        let bottom = y + self.line_height.max(1.0);
88
89        self.paint.set_color(self.foreground);
90        self.canvas.draw_rect(
91            SkRect::new(left, y.round(), right, bottom.round()),
92            self.paint,
93        );
94
95        let glyph = match cell.c {
96            '\0' | '\t' => ' ',
97            c => c,
98        };
99        let mut buf = [0u8; 4];
100        let content: &str = glyph.encode_utf8(&mut buf);
101
102        self.paint.set_color(self.background);
103        if let Some(blob) = TextBlob::from_pos_text_h(content, &[0.0], 0.0, self.font) {
104            self.canvas
105                .draw_text_blob(&blob, (left, y + self.baseline_offset), self.paint);
106        }
107    }
108
109    pub fn render_scrollbar(
110        &mut self,
111        scroll_offset: usize,
112        total_scrollback: usize,
113        rows_count: usize,
114    ) {
115        let viewport_height = self.area.height();
116        let total_rows = rows_count + total_scrollback;
117        let total_content_height = total_rows as f32 * self.line_height;
118
119        let scrollbar_height = (viewport_height * viewport_height / total_content_height).max(20.0);
120        let track_height = viewport_height - scrollbar_height;
121        let scroll_ratio = scroll_offset as f32 / total_scrollback as f32;
122        let thumb_y = self.area.min_y() + track_height * (1.0 - scroll_ratio);
123
124        let scrollbar_x = self.area.max_x() - 4.0;
125        let corner_radius = 2.0;
126
127        self.paint.set_anti_alias(true);
128        self.paint.set_color(Color::from_argb(50, 0, 0, 0));
129        self.canvas.draw_round_rect(
130            SkRect::new(
131                scrollbar_x,
132                self.area.min_y(),
133                self.area.max_x(),
134                self.area.max_y(),
135            ),
136            corner_radius,
137            corner_radius,
138            self.paint,
139        );
140
141        self.paint.set_color(Color::from_argb(60, 255, 255, 255));
142        self.canvas.draw_round_rect(
143            SkRect::new(
144                scrollbar_x,
145                thumb_y,
146                self.area.max_x(),
147                thumb_y + scrollbar_height,
148            ),
149            corner_radius,
150            corner_radius,
151            self.paint,
152        );
153    }
154
155    fn render_cell_backgrounds(&mut self, row: &[Cell], y: f32) {
156        let mut run_start: Option<(usize, Color)> = None;
157        let mut col = 0;
158        while col < row.len() {
159            let cell = &row[col];
160            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
161                col += 1;
162                continue;
163            }
164            let cell_bg = if cell.flags.contains(Flags::INVERSE) {
165                map_ansi_color(cell.fg, self.foreground, self.background)
166            } else {
167                map_ansi_color(cell.bg, self.foreground, self.background)
168            };
169            let end_col = if cell.flags.contains(Flags::WIDE_CHAR) {
170                col + 2
171            } else {
172                col + 1
173            };
174
175            if cell_bg != self.background {
176                match &run_start {
177                    Some((_, color)) if *color == cell_bg => {}
178                    Some((start, color)) => {
179                        self.fill_cells(*start, col, *color, y);
180                        run_start = Some((col, cell_bg));
181                    }
182                    None => {
183                        run_start = Some((col, cell_bg));
184                    }
185                }
186            } else if let Some((start, color)) = run_start.take() {
187                self.fill_cells(start, col, color, y);
188            }
189            col = end_col;
190        }
191        if let Some((start, color)) = run_start {
192            self.fill_cells(start, col, color, y);
193        }
194    }
195
196    fn fill_cells(&mut self, start: usize, end: usize, color: Color, y: f32) {
197        let left = self.area.min_x() + (start as f32) * self.char_width;
198        let right = self.area.min_x() + (end as f32) * self.char_width;
199        self.paint.set_color(color);
200        self.canvas.draw_rect(
201            SkRect::new(left, y.round(), right, (y + self.line_height).round()),
202            self.paint,
203        );
204    }
205
206    fn render_selection(&mut self, row_idx: usize, row_len: usize, y: f32) {
207        let Some(range) = self.selection else {
208            return;
209        };
210        let offset = self.display_offset as i64;
211        let start_row = range.start.line.0 as i64 + offset;
212        let end_row = range.end.line.0 as i64 + offset;
213        let row_i = row_idx as i64;
214        if row_i < start_row || row_i > end_row {
215            return;
216        }
217        let sel_start = if row_i == start_row {
218            range.start.column.0
219        } else {
220            0
221        };
222        let sel_end = if row_i == end_row {
223            (range.end.column.0 + 1).min(row_len)
224        } else {
225            row_len
226        };
227        if sel_start >= sel_end {
228            return;
229        }
230        let left = self.area.min_x() + (sel_start as f32) * self.char_width;
231        let right = self.area.min_x() + (sel_end as f32) * self.char_width;
232        self.paint.set_color(self.selection_color);
233        self.canvas.draw_rect(
234            SkRect::new(left, y.round(), right, (y + self.line_height).round()),
235            self.paint,
236        );
237    }
238
239    /// Draw a row's glyphs, hashing the row contents to hit `row_cache` on
240    /// repeat frames. Picks the fast `TextBlob` path or the `Paragraph` path
241    /// when font fallback (emoji, wide chars) is needed.
242    fn render_text_row(&mut self, row: &[Cell], y: f32) {
243        let mut hasher = FxHasher::default();
244        let mut needs_fallback = false;
245        for cell in row.iter() {
246            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
247                continue;
248            }
249            let text = cell_text(cell);
250            let cell_fg = self.cell_foreground(cell);
251            text.hash(&mut hasher);
252            cell_fg.hash(&mut hasher);
253            if !needs_fallback {
254                needs_fallback = cell.flags.contains(Flags::WIDE_CHAR)
255                    || (!text.is_ascii() && self.font.text_to_glyphs_vec(&text).contains(&0));
256            }
257        }
258        let cache_key = hasher.finish();
259        let text_y = y + self.baseline_offset;
260        let area_min_x = self.area.min_x();
261
262        if let Some(cached) = self.row_cache.get(&cache_key) {
263            match cached {
264                CachedRow::TextBlobs(blobs) => {
265                    for (blob, color) in blobs {
266                        self.paint.set_color(*color);
267                        self.canvas
268                            .draw_text_blob(blob, (area_min_x, text_y), self.paint);
269                    }
270                }
271                CachedRow::Paragraph(paragraph) => {
272                    paragraph.paint(self.canvas, (area_min_x, y));
273                }
274            }
275        } else if needs_fallback {
276            self.render_paragraph(row, y, cache_key);
277        } else {
278            self.render_textblob(row, text_y, cache_key);
279        }
280    }
281
282    fn cell_foreground(&self, cell: &Cell) -> Color {
283        let raw = if cell.flags.contains(Flags::INVERSE) {
284            cell.bg
285        } else {
286            cell.fg
287        };
288        map_ansi_color(raw, self.foreground, self.background)
289    }
290
291    /// Fast path: same-color glyphs batched into one `TextBlob`, each glyph
292    /// pinned to its grid x-offset to preserve monospace alignment.
293    fn render_textblob(&mut self, row: &[Cell], text_y: f32, cache_key: u64) {
294        let mut current_color: Option<Color> = None;
295        let mut glyphs = String::new();
296        let mut glyph_positions: Vec<f32> = Vec::new();
297        let mut blobs: Vec<(TextBlob, Color)> = Vec::new();
298
299        for (col_idx, cell) in row.iter().enumerate() {
300            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
301                continue;
302            }
303            let cell_fg = self.cell_foreground(cell);
304            let text = cell_text(cell);
305            let x = (col_idx as f32) * self.char_width;
306
307            if current_color != Some(cell_fg) {
308                if let Some(prev_color) = current_color {
309                    self.flush_blob(&glyphs, &glyph_positions, text_y, &mut blobs, prev_color);
310                    glyphs.clear();
311                    glyph_positions.clear();
312                }
313                current_color = Some(cell_fg);
314            }
315            for _ in text.chars() {
316                glyph_positions.push(x);
317            }
318            glyphs.push_str(&text);
319        }
320
321        if let Some(color) = current_color
322            && !glyphs.is_empty()
323        {
324            self.flush_blob(&glyphs, &glyph_positions, text_y, &mut blobs, color);
325        }
326
327        self.row_cache
328            .insert(cache_key, CachedRow::TextBlobs(blobs));
329    }
330
331    fn flush_blob(
332        &mut self,
333        glyphs: &str,
334        glyph_positions: &[f32],
335        text_y: f32,
336        blobs: &mut Vec<(TextBlob, Color)>,
337        color: Color,
338    ) {
339        if let Some(blob) = TextBlob::from_pos_text_h(glyphs, glyph_positions, 0.0, self.font) {
340            self.paint.set_color(color);
341            self.canvas
342                .draw_text_blob(&blob, (self.area.min_x(), text_y), self.paint);
343            blobs.push((blob, color));
344        }
345    }
346
347    /// Slow path: Paragraph with font fallback for emoji/wide chars.
348    fn render_paragraph(&mut self, row: &[Cell], row_y: f32, cache_key: u64) {
349        let mut text_style = TextStyle::new();
350        text_style.set_font_size(self.font_size);
351        text_style.set_font_families(&[self.font_family]);
352        text_style.set_color(self.foreground);
353
354        let mut builder =
355            ParagraphBuilder::new(&ParagraphStyle::default(), self.font_collection.clone());
356
357        for cell in row.iter() {
358            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
359                continue;
360            }
361            let mut cell_style = text_style.clone();
362            cell_style.set_color(self.cell_foreground(cell));
363            builder.push_style(&cell_style);
364            builder.add_text(cell_text(cell).as_str());
365        }
366
367        let mut paragraph = builder.build();
368        paragraph.layout(f32::MAX);
369        paragraph.paint(self.canvas, (self.area.min_x(), row_y));
370
371        self.row_cache
372            .insert(cache_key, CachedRow::Paragraph(paragraph));
373    }
374}
375
376/// Visible text for a cell, mapping empty (`\0`) and tab (`\t`) cells to a space.
377fn cell_text(cell: &Cell) -> String {
378    let mut s = String::new();
379    s.push(match cell.c {
380        '\0' | '\t' => ' ',
381        c => c,
382    });
383    if let Some(extra) = cell.zerowidth() {
384        for c in extra {
385            s.push(*c);
386        }
387    }
388    s
389}