Skip to main content

freya_terminal/
rendering.rs

1use std::hash::{
2    Hash,
3    Hasher,
4};
5
6use freya_core::{
7    fifo_cache::FifoCache,
8    prelude::Color,
9};
10use freya_engine::prelude::{
11    Canvas,
12    Font,
13    FontCollection,
14    Paint,
15    Paragraph,
16    ParagraphBuilder,
17    ParagraphStyle,
18    TextBlob,
19    TextStyle,
20};
21use rustc_hash::FxHasher;
22
23use crate::colors::map_vt100_color;
24
25pub(crate) enum CachedRow {
26    TextBlobs(Vec<(TextBlob, Color)>),
27    Paragraph(Paragraph),
28}
29
30/// Renders terminal text using TextBlob (fast) or Paragraph (font fallback).
31pub(crate) struct TextRenderer<'a> {
32    pub canvas: &'a Canvas,
33    pub font: &'a Font,
34    pub font_collection: &'a mut FontCollection,
35    pub paint: &'a mut Paint,
36    pub row_cache: &'a mut FifoCache<u64, CachedRow>,
37    pub area_min_x: f32,
38    pub char_width: f32,
39    pub line_height: f32,
40    pub baseline_offset: f32,
41    pub foreground: Color,
42    pub background: Color,
43    pub font_family: &'a str,
44    pub font_size: f32,
45}
46
47impl TextRenderer<'_> {
48    fn cell_text(cell: &vt100::Cell) -> &str {
49        if cell.has_contents() {
50            cell.contents()
51        } else {
52            " "
53        }
54    }
55
56    fn cell_foreground(&self, cell: &vt100::Cell) -> Color {
57        if cell.inverse() {
58            map_vt100_color(cell.bgcolor(), self.background)
59        } else {
60            map_vt100_color(cell.fgcolor(), self.foreground)
61        }
62    }
63
64    fn render_blob(
65        &mut self,
66        glyphs: &str,
67        glyph_positions: &[f32],
68        text_y: f32,
69        blobs: &mut Vec<(TextBlob, Color)>,
70        color: Color,
71    ) {
72        if let Some(blob) = TextBlob::from_pos_text_h(glyphs, glyph_positions, 0.0, self.font) {
73            self.paint.set_color(color);
74            self.canvas
75                .draw_text_blob(&blob, (self.area_min_x, text_y), self.paint);
76            blobs.push((blob, color));
77        }
78    }
79
80    pub fn render_text(
81        &mut self,
82        rows: &[Vec<vt100::Cell>],
83        area_min_y: f32,
84        area_max_y: f32,
85        mut pre_row: impl FnMut(&[vt100::Cell], f32, &Canvas, &mut Paint),
86        mut post_row: impl FnMut(usize, &[vt100::Cell], f32, &Canvas, &mut Paint),
87    ) {
88        let mut y = area_min_y;
89
90        for (row_idx, row) in rows.iter().enumerate() {
91            if y + self.line_height > area_max_y {
92                break;
93            }
94
95            pre_row(row, y, self.canvas, self.paint);
96
97            let mut hasher = FxHasher::default();
98            let mut needs_fallback = false;
99            for cell in row.iter() {
100                if cell.is_wide_continuation() {
101                    continue;
102                }
103                let contents = cell.contents();
104                let cell_fg = self.cell_foreground(cell);
105                contents.hash(&mut hasher);
106                cell_fg.hash(&mut hasher);
107                if !needs_fallback {
108                    needs_fallback = cell.is_wide()
109                        || (!contents.is_ascii()
110                            && self.font.text_to_glyphs_vec(contents).contains(&0));
111                }
112            }
113            let cache_key = hasher.finish();
114            let text_y = y + self.baseline_offset;
115
116            if let Some(cached) = self.row_cache.get(&cache_key) {
117                match cached {
118                    CachedRow::TextBlobs(blobs) => {
119                        for (blob, color) in blobs {
120                            self.paint.set_color(*color);
121                            self.canvas
122                                .draw_text_blob(blob, (self.area_min_x, text_y), self.paint);
123                        }
124                    }
125                    CachedRow::Paragraph(paragraph) => {
126                        paragraph.paint(self.canvas, (self.area_min_x, y));
127                    }
128                }
129            } else if needs_fallback {
130                self.render_paragraph(row, y, cache_key);
131            } else {
132                self.render_textblob(row, text_y, cache_key);
133            }
134
135            post_row(row_idx, row, y, self.canvas, self.paint);
136
137            y += self.line_height;
138        }
139    }
140
141    /// Fast path: TextBlob with explicit grid positions per glyph.
142    fn render_textblob(&mut self, row: &[vt100::Cell], text_y: f32, cache_key: u64) {
143        let mut current_color: Option<Color> = None;
144
145        // Same-color glyphs are batched into a single TextBlob.
146        // Each char gets a grid x-offset to preserve monospace alignment.
147        let mut glyphs = String::new();
148        let mut glyph_positions: Vec<f32> = Vec::new();
149
150        let mut blobs: Vec<(TextBlob, Color)> = Vec::new();
151
152        for (col_idx, cell) in row.iter().enumerate() {
153            if cell.is_wide_continuation() {
154                continue;
155            }
156            let cell_fg = self.cell_foreground(cell);
157            let text = Self::cell_text(cell);
158            let x = (col_idx as f32) * self.char_width;
159
160            if current_color != Some(cell_fg) {
161                if let Some(prev_color) = current_color {
162                    self.render_blob(&glyphs, &glyph_positions, text_y, &mut blobs, prev_color);
163                    glyphs.clear();
164                    glyph_positions.clear();
165                }
166                current_color = Some(cell_fg);
167            }
168            for _ in text.chars() {
169                glyph_positions.push(x);
170            }
171            glyphs.push_str(text);
172        }
173
174        if !glyphs.is_empty() {
175            self.render_blob(
176                &glyphs,
177                &glyph_positions,
178                text_y,
179                &mut blobs,
180                current_color.unwrap(),
181            );
182        }
183
184        self.row_cache
185            .insert(cache_key, CachedRow::TextBlobs(blobs));
186    }
187
188    /// Slow path: Paragraph with font fallback for emoji/wide chars.
189    fn render_paragraph(&mut self, row: &[vt100::Cell], row_y: f32, cache_key: u64) {
190        let mut text_style = TextStyle::new();
191        text_style.set_font_size(self.font_size);
192        text_style.set_font_families(&[self.font_family]);
193        text_style.set_color(self.foreground);
194
195        let mut builder =
196            ParagraphBuilder::new(&ParagraphStyle::default(), self.font_collection.clone());
197
198        for cell in row.iter() {
199            if cell.is_wide_continuation() {
200                continue;
201            }
202            let mut cell_style = text_style.clone();
203            cell_style.set_color(self.cell_foreground(cell));
204            builder.push_style(&cell_style);
205            builder.add_text(Self::cell_text(cell));
206        }
207
208        let mut paragraph = builder.build();
209        paragraph.layout(f32::MAX);
210        paragraph.paint(self.canvas, (self.area_min_x, row_y));
211
212        self.row_cache
213            .insert(cache_key, CachedRow::Paragraph(paragraph));
214    }
215}