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 = if cell.c == '\0' { ' ' } else { cell.c };
96        let mut buf = [0u8; 4];
97        let content: &str = glyph.encode_utf8(&mut buf);
98
99        self.paint.set_color(self.background);
100        if let Some(blob) = TextBlob::from_pos_text_h(content, &[0.0], 0.0, self.font) {
101            self.canvas
102                .draw_text_blob(&blob, (left, y + self.baseline_offset), self.paint);
103        }
104    }
105
106    pub fn render_scrollbar(
107        &mut self,
108        scroll_offset: usize,
109        total_scrollback: usize,
110        rows_count: usize,
111    ) {
112        let viewport_height = self.area.height();
113        let total_rows = rows_count + total_scrollback;
114        let total_content_height = total_rows as f32 * self.line_height;
115
116        let scrollbar_height = (viewport_height * viewport_height / total_content_height).max(20.0);
117        let track_height = viewport_height - scrollbar_height;
118        let scroll_ratio = scroll_offset as f32 / total_scrollback as f32;
119        let thumb_y = self.area.min_y() + track_height * (1.0 - scroll_ratio);
120
121        let scrollbar_x = self.area.max_x() - 4.0;
122        let corner_radius = 2.0;
123
124        self.paint.set_anti_alias(true);
125        self.paint.set_color(Color::from_argb(50, 0, 0, 0));
126        self.canvas.draw_round_rect(
127            SkRect::new(
128                scrollbar_x,
129                self.area.min_y(),
130                self.area.max_x(),
131                self.area.max_y(),
132            ),
133            corner_radius,
134            corner_radius,
135            self.paint,
136        );
137
138        self.paint.set_color(Color::from_argb(60, 255, 255, 255));
139        self.canvas.draw_round_rect(
140            SkRect::new(
141                scrollbar_x,
142                thumb_y,
143                self.area.max_x(),
144                thumb_y + scrollbar_height,
145            ),
146            corner_radius,
147            corner_radius,
148            self.paint,
149        );
150    }
151
152    fn render_cell_backgrounds(&mut self, row: &[Cell], y: f32) {
153        let mut run_start: Option<(usize, Color)> = None;
154        let mut col = 0;
155        while col < row.len() {
156            let cell = &row[col];
157            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
158                col += 1;
159                continue;
160            }
161            let cell_bg = if cell.flags.contains(Flags::INVERSE) {
162                map_ansi_color(cell.fg, self.foreground, self.background)
163            } else {
164                map_ansi_color(cell.bg, self.foreground, self.background)
165            };
166            let end_col = if cell.flags.contains(Flags::WIDE_CHAR) {
167                col + 2
168            } else {
169                col + 1
170            };
171
172            if cell_bg != self.background {
173                match &run_start {
174                    Some((_, color)) if *color == cell_bg => {}
175                    Some((start, color)) => {
176                        self.fill_cells(*start, col, *color, y);
177                        run_start = Some((col, cell_bg));
178                    }
179                    None => {
180                        run_start = Some((col, cell_bg));
181                    }
182                }
183            } else if let Some((start, color)) = run_start.take() {
184                self.fill_cells(start, col, color, y);
185            }
186            col = end_col;
187        }
188        if let Some((start, color)) = run_start {
189            self.fill_cells(start, col, color, y);
190        }
191    }
192
193    fn fill_cells(&mut self, start: usize, end: usize, color: Color, y: f32) {
194        let left = self.area.min_x() + (start as f32) * self.char_width;
195        let right = self.area.min_x() + (end as f32) * self.char_width;
196        self.paint.set_color(color);
197        self.canvas.draw_rect(
198            SkRect::new(left, y.round(), right, (y + self.line_height).round()),
199            self.paint,
200        );
201    }
202
203    fn render_selection(&mut self, row_idx: usize, row_len: usize, y: f32) {
204        let Some(range) = self.selection else {
205            return;
206        };
207        let offset = self.display_offset as i64;
208        let start_row = range.start.line.0 as i64 + offset;
209        let end_row = range.end.line.0 as i64 + offset;
210        let row_i = row_idx as i64;
211        if row_i < start_row || row_i > end_row {
212            return;
213        }
214        let sel_start = if row_i == start_row {
215            range.start.column.0
216        } else {
217            0
218        };
219        let sel_end = if row_i == end_row {
220            range.end.column.0.min(row_len)
221        } else {
222            row_len
223        };
224        if sel_start >= sel_end {
225            return;
226        }
227        let left = self.area.min_x() + (sel_start as f32) * self.char_width;
228        let right = self.area.min_x() + (sel_end as f32) * self.char_width;
229        self.paint.set_color(self.selection_color);
230        self.canvas.draw_rect(
231            SkRect::new(left, y.round(), right, (y + self.line_height).round()),
232            self.paint,
233        );
234    }
235
236    /// Draw a row's glyphs, hashing the row contents to hit `row_cache` on
237    /// repeat frames. Picks the fast `TextBlob` path or the `Paragraph` path
238    /// when font fallback (emoji, wide chars) is needed.
239    fn render_text_row(&mut self, row: &[Cell], y: f32) {
240        let mut hasher = FxHasher::default();
241        let mut needs_fallback = false;
242        for cell in row.iter() {
243            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
244                continue;
245            }
246            let text = cell_text(cell);
247            let cell_fg = self.cell_foreground(cell);
248            text.hash(&mut hasher);
249            cell_fg.hash(&mut hasher);
250            if !needs_fallback {
251                needs_fallback = cell.flags.contains(Flags::WIDE_CHAR)
252                    || (!text.is_ascii() && self.font.text_to_glyphs_vec(&text).contains(&0));
253            }
254        }
255        let cache_key = hasher.finish();
256        let text_y = y + self.baseline_offset;
257        let area_min_x = self.area.min_x();
258
259        if let Some(cached) = self.row_cache.get(&cache_key) {
260            match cached {
261                CachedRow::TextBlobs(blobs) => {
262                    for (blob, color) in blobs {
263                        self.paint.set_color(*color);
264                        self.canvas
265                            .draw_text_blob(blob, (area_min_x, text_y), self.paint);
266                    }
267                }
268                CachedRow::Paragraph(paragraph) => {
269                    paragraph.paint(self.canvas, (area_min_x, y));
270                }
271            }
272        } else if needs_fallback {
273            self.render_paragraph(row, y, cache_key);
274        } else {
275            self.render_textblob(row, text_y, cache_key);
276        }
277    }
278
279    fn cell_foreground(&self, cell: &Cell) -> Color {
280        let raw = if cell.flags.contains(Flags::INVERSE) {
281            cell.bg
282        } else {
283            cell.fg
284        };
285        map_ansi_color(raw, self.foreground, self.background)
286    }
287
288    /// Fast path: same-color glyphs batched into one `TextBlob`, each glyph
289    /// pinned to its grid x-offset to preserve monospace alignment.
290    fn render_textblob(&mut self, row: &[Cell], text_y: f32, cache_key: u64) {
291        let mut current_color: Option<Color> = None;
292        let mut glyphs = String::new();
293        let mut glyph_positions: Vec<f32> = Vec::new();
294        let mut blobs: Vec<(TextBlob, Color)> = Vec::new();
295
296        for (col_idx, cell) in row.iter().enumerate() {
297            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
298                continue;
299            }
300            let cell_fg = self.cell_foreground(cell);
301            let text = cell_text(cell);
302            let x = (col_idx as f32) * self.char_width;
303
304            if current_color != Some(cell_fg) {
305                if let Some(prev_color) = current_color {
306                    self.flush_blob(&glyphs, &glyph_positions, text_y, &mut blobs, prev_color);
307                    glyphs.clear();
308                    glyph_positions.clear();
309                }
310                current_color = Some(cell_fg);
311            }
312            for _ in text.chars() {
313                glyph_positions.push(x);
314            }
315            glyphs.push_str(&text);
316        }
317
318        if let Some(color) = current_color
319            && !glyphs.is_empty()
320        {
321            self.flush_blob(&glyphs, &glyph_positions, text_y, &mut blobs, color);
322        }
323
324        self.row_cache
325            .insert(cache_key, CachedRow::TextBlobs(blobs));
326    }
327
328    fn flush_blob(
329        &mut self,
330        glyphs: &str,
331        glyph_positions: &[f32],
332        text_y: f32,
333        blobs: &mut Vec<(TextBlob, Color)>,
334        color: Color,
335    ) {
336        if let Some(blob) = TextBlob::from_pos_text_h(glyphs, glyph_positions, 0.0, self.font) {
337            self.paint.set_color(color);
338            self.canvas
339                .draw_text_blob(&blob, (self.area.min_x(), text_y), self.paint);
340            blobs.push((blob, color));
341        }
342    }
343
344    /// Slow path: Paragraph with font fallback for emoji/wide chars.
345    fn render_paragraph(&mut self, row: &[Cell], row_y: f32, cache_key: u64) {
346        let mut text_style = TextStyle::new();
347        text_style.set_font_size(self.font_size);
348        text_style.set_font_families(&[self.font_family]);
349        text_style.set_color(self.foreground);
350
351        let mut builder =
352            ParagraphBuilder::new(&ParagraphStyle::default(), self.font_collection.clone());
353
354        for cell in row.iter() {
355            if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
356                continue;
357            }
358            let mut cell_style = text_style.clone();
359            cell_style.set_color(self.cell_foreground(cell));
360            builder.push_style(&cell_style);
361            builder.add_text(cell_text(cell).as_str());
362        }
363
364        let mut paragraph = builder.build();
365        paragraph.layout(f32::MAX);
366        paragraph.paint(self.canvas, (self.area.min_x(), row_y));
367
368        self.row_cache
369            .insert(cache_key, CachedRow::Paragraph(paragraph));
370    }
371}
372
373/// Printable text for a cell, treating empty cells as a space and appending
374/// any combining (zero-width) characters.
375fn cell_text(cell: &Cell) -> String {
376    let mut s = String::new();
377    s.push(if cell.c == '\0' { ' ' } else { cell.c });
378    if let Some(extra) = cell.zerowidth() {
379        for c in extra {
380            s.push(*c);
381        }
382    }
383    s
384}