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::{
33 colors::map_ansi_color,
34 url::link_ranges,
35};
36
37pub(crate) enum CachedRow {
39 TextBlobs(Vec<(TextBlob, Color)>),
41 Paragraph(Paragraph),
43}
44
45pub(crate) struct Renderer<'a> {
48 pub canvas: &'a Canvas,
49 pub paint: &'a mut Paint,
50 pub font: &'a Font,
51 pub font_collection: &'a mut FontCollection,
52 pub row_cache: &'a mut FifoCache<u64, CachedRow>,
53 pub area: Area,
54 pub char_width: f32,
55 pub line_height: f32,
56 pub baseline_offset: f32,
57 pub foreground: Color,
58 pub background: Color,
59 pub selection_color: Color,
60 pub font_family: &'a str,
61 pub font_size: f32,
62 pub selection: Option<SelectionRange>,
63 pub display_offset: usize,
64}
65
66impl Renderer<'_> {
67 pub fn render_background(&mut self) {
68 self.paint.set_color(self.background);
69 self.canvas.draw_rect(
70 SkRect::new(
71 self.area.min_x(),
72 self.area.min_y(),
73 self.area.max_x(),
74 self.area.max_y(),
75 ),
76 self.paint,
77 );
78 }
79
80 pub fn render_row(&mut self, row_idx: usize, row: &[Cell], y: f32) {
82 self.render_cell_backgrounds(row, y);
83 self.render_text_row(row, y);
84 self.render_hyperlink_underlines(row, y);
85 self.render_selection(row_idx, row.len(), y);
86 }
87
88 pub fn render_cursor(&mut self, cell: &Cell, y: f32, cursor_col: usize) {
89 let left = self.area.min_x() + (cursor_col as f32) * self.char_width;
90 let right = left + self.char_width.max(1.0);
91 let bottom = y + self.line_height.max(1.0);
92
93 self.paint.set_color(self.foreground);
94 self.canvas.draw_rect(
95 SkRect::new(left, y.round(), right, bottom.round()),
96 self.paint,
97 );
98
99 let glyph = match cell.c {
100 '\0' | '\t' => ' ',
101 c => c,
102 };
103 let mut buf = [0u8; 4];
104 let content: &str = glyph.encode_utf8(&mut buf);
105
106 self.paint.set_color(self.background);
107 if let Some(blob) = TextBlob::from_pos_text_h(content, &[0.0], 0.0, self.font) {
108 self.canvas
109 .draw_text_blob(&blob, (left, y + self.baseline_offset), self.paint);
110 }
111 }
112
113 pub fn render_scrollbar(
114 &mut self,
115 scroll_offset: usize,
116 total_scrollback: usize,
117 rows_count: usize,
118 ) {
119 let viewport_height = self.area.height();
120 let total_rows = rows_count + total_scrollback;
121 let total_content_height = total_rows as f32 * self.line_height;
122
123 let scrollbar_height = (viewport_height * viewport_height / total_content_height).max(20.0);
124 let track_height = viewport_height - scrollbar_height;
125 let scroll_ratio = scroll_offset as f32 / total_scrollback as f32;
126 let thumb_y = self.area.min_y() + track_height * (1.0 - scroll_ratio);
127
128 let scrollbar_x = self.area.max_x() - 4.0;
129 let corner_radius = 2.0;
130
131 self.paint.set_anti_alias(true);
132 self.paint.set_color(Color::from_argb(50, 0, 0, 0));
133 self.canvas.draw_round_rect(
134 SkRect::new(
135 scrollbar_x,
136 self.area.min_y(),
137 self.area.max_x(),
138 self.area.max_y(),
139 ),
140 corner_radius,
141 corner_radius,
142 self.paint,
143 );
144
145 self.paint.set_color(Color::from_argb(60, 255, 255, 255));
146 self.canvas.draw_round_rect(
147 SkRect::new(
148 scrollbar_x,
149 thumb_y,
150 self.area.max_x(),
151 thumb_y + scrollbar_height,
152 ),
153 corner_radius,
154 corner_radius,
155 self.paint,
156 );
157 }
158
159 fn render_cell_backgrounds(&mut self, row: &[Cell], y: f32) {
160 let mut run_start: Option<(usize, Color)> = None;
161 let mut col = 0;
162 while col < row.len() {
163 let cell = &row[col];
164 if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
165 col += 1;
166 continue;
167 }
168 let cell_bg = if cell.flags.contains(Flags::INVERSE) {
169 map_ansi_color(cell.fg, self.foreground, self.background)
170 } else {
171 map_ansi_color(cell.bg, self.foreground, self.background)
172 };
173 let end_col = if cell.flags.contains(Flags::WIDE_CHAR) {
174 col + 2
175 } else {
176 col + 1
177 };
178
179 if cell_bg != self.background {
180 match &run_start {
181 Some((_, color)) if *color == cell_bg => {}
182 Some((start, color)) => {
183 self.fill_cells(*start, col, *color, y);
184 run_start = Some((col, cell_bg));
185 }
186 None => {
187 run_start = Some((col, cell_bg));
188 }
189 }
190 } else if let Some((start, color)) = run_start.take() {
191 self.fill_cells(start, col, color, y);
192 }
193 col = end_col;
194 }
195 if let Some((start, color)) = run_start {
196 self.fill_cells(start, col, color, y);
197 }
198 }
199
200 fn fill_cells(&mut self, start: usize, end: usize, color: Color, y: f32) {
201 let left = self.area.min_x() + (start as f32) * self.char_width;
202 let right = self.area.min_x() + (end as f32) * self.char_width;
203 self.paint.set_color(color);
204 self.canvas.draw_rect(
205 SkRect::new(left, y.round(), right, (y + self.line_height).round()),
206 self.paint,
207 );
208 }
209
210 fn render_hyperlink_underlines(&mut self, row: &[Cell], y: f32) {
212 let underline_y = (y + self.line_height - 2.0).round();
213 self.paint.set_color(self.foreground);
214 for (start, end) in link_ranges(row) {
215 let left = self.area.min_x() + (start as f32) * self.char_width;
216 let right = self.area.min_x() + (end as f32) * self.char_width;
217 self.canvas.draw_rect(
218 SkRect::new(left, underline_y, right, underline_y + 1.0),
219 self.paint,
220 );
221 }
222 }
223
224 fn render_selection(&mut self, row_idx: usize, row_len: usize, y: f32) {
225 let Some(range) = self.selection else {
226 return;
227 };
228 let offset = self.display_offset as i64;
229 let start_row = range.start.line.0 as i64 + offset;
230 let end_row = range.end.line.0 as i64 + offset;
231 let row_i = row_idx as i64;
232 if row_i < start_row || row_i > end_row {
233 return;
234 }
235 let sel_start = if row_i == start_row {
236 range.start.column.0
237 } else {
238 0
239 };
240 let sel_end = if row_i == end_row {
241 (range.end.column.0 + 1).min(row_len)
242 } else {
243 row_len
244 };
245 if sel_start >= sel_end {
246 return;
247 }
248 let left = self.area.min_x() + (sel_start as f32) * self.char_width;
249 let right = self.area.min_x() + (sel_end as f32) * self.char_width;
250 self.paint.set_color(self.selection_color);
251 self.canvas.draw_rect(
252 SkRect::new(left, y.round(), right, (y + self.line_height).round()),
253 self.paint,
254 );
255 }
256
257 fn render_text_row(&mut self, row: &[Cell], y: f32) {
261 let mut hasher = FxHasher::default();
262 let mut needs_fallback = false;
263 for cell in row.iter() {
264 if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
265 continue;
266 }
267 let text = cell_text(cell);
268 let cell_fg = self.cell_foreground(cell);
269 text.hash(&mut hasher);
270 cell_fg.hash(&mut hasher);
271 if !needs_fallback {
272 needs_fallback = cell.flags.contains(Flags::WIDE_CHAR)
273 || (!text.is_ascii() && self.font.text_to_glyphs_vec(&text).contains(&0));
274 }
275 }
276 let cache_key = hasher.finish();
277 let text_y = y + self.baseline_offset;
278 let area_min_x = self.area.min_x();
279
280 if let Some(cached) = self.row_cache.get(&cache_key) {
281 match cached {
282 CachedRow::TextBlobs(blobs) => {
283 for (blob, color) in blobs {
284 self.paint.set_color(*color);
285 self.canvas
286 .draw_text_blob(blob, (area_min_x, text_y), self.paint);
287 }
288 }
289 CachedRow::Paragraph(paragraph) => {
290 paragraph.paint(self.canvas, (area_min_x, y));
291 }
292 }
293 } else if needs_fallback {
294 self.render_paragraph(row, y, cache_key);
295 } else {
296 self.render_textblob(row, text_y, cache_key);
297 }
298 }
299
300 fn cell_foreground(&self, cell: &Cell) -> Color {
301 let raw = if cell.flags.contains(Flags::INVERSE) {
302 cell.bg
303 } else {
304 cell.fg
305 };
306 map_ansi_color(raw, self.foreground, self.background)
307 }
308
309 fn render_textblob(&mut self, row: &[Cell], text_y: f32, cache_key: u64) {
312 let mut current_color: Option<Color> = None;
313 let mut glyphs = String::new();
314 let mut glyph_positions: Vec<f32> = Vec::new();
315 let mut blobs: Vec<(TextBlob, Color)> = Vec::new();
316
317 for (col_idx, cell) in row.iter().enumerate() {
318 if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
319 continue;
320 }
321 let cell_fg = self.cell_foreground(cell);
322 let text = cell_text(cell);
323 let x = (col_idx as f32) * self.char_width;
324
325 if current_color != Some(cell_fg) {
326 if let Some(prev_color) = current_color {
327 self.flush_blob(&glyphs, &glyph_positions, text_y, &mut blobs, prev_color);
328 glyphs.clear();
329 glyph_positions.clear();
330 }
331 current_color = Some(cell_fg);
332 }
333 for _ in text.chars() {
334 glyph_positions.push(x);
335 }
336 glyphs.push_str(&text);
337 }
338
339 if let Some(color) = current_color
340 && !glyphs.is_empty()
341 {
342 self.flush_blob(&glyphs, &glyph_positions, text_y, &mut blobs, color);
343 }
344
345 self.row_cache
346 .insert(cache_key, CachedRow::TextBlobs(blobs));
347 }
348
349 fn flush_blob(
350 &mut self,
351 glyphs: &str,
352 glyph_positions: &[f32],
353 text_y: f32,
354 blobs: &mut Vec<(TextBlob, Color)>,
355 color: Color,
356 ) {
357 if let Some(blob) = TextBlob::from_pos_text_h(glyphs, glyph_positions, 0.0, self.font) {
358 self.paint.set_color(color);
359 self.canvas
360 .draw_text_blob(&blob, (self.area.min_x(), text_y), self.paint);
361 blobs.push((blob, color));
362 }
363 }
364
365 fn render_paragraph(&mut self, row: &[Cell], row_y: f32, cache_key: u64) {
367 let mut text_style = TextStyle::new();
368 text_style.set_font_size(self.font_size);
369 text_style.set_font_families(&[self.font_family]);
370 text_style.set_color(self.foreground);
371
372 let mut builder =
373 ParagraphBuilder::new(&ParagraphStyle::default(), self.font_collection.clone());
374
375 for cell in row.iter() {
376 if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
377 continue;
378 }
379 let mut cell_style = text_style.clone();
380 cell_style.set_color(self.cell_foreground(cell));
381 builder.push_style(&cell_style);
382 builder.add_text(cell_text(cell).as_str());
383 }
384
385 let mut paragraph = builder.build();
386 paragraph.layout(f32::MAX);
387 paragraph.paint(self.canvas, (self.area.min_x(), row_y));
388
389 self.row_cache
390 .insert(cache_key, CachedRow::Paragraph(paragraph));
391 }
392}
393
394fn cell_text(cell: &Cell) -> String {
396 let mut s = String::new();
397 s.push(match cell.c {
398 '\0' | '\t' => ' ',
399 c => c,
400 });
401 if let Some(extra) = cell.zerowidth() {
402 for c in extra {
403 s.push(*c);
404 }
405 }
406 s
407}