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
34pub(crate) enum CachedRow {
36 TextBlobs(Vec<(TextBlob, Color)>),
38 Paragraph(Paragraph),
40}
41
42pub(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 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 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 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 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
373fn 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}