1use std::{
2 any::Any,
3 borrow::Cow,
4 cell::RefCell,
5 rc::Rc,
6};
7
8use freya_core::{
9 data::{
10 AccessibilityData,
11 LayoutData,
12 },
13 diff_key::DiffKey,
14 element::{
15 Element,
16 ElementExt,
17 EventHandlerType,
18 LayoutContext,
19 RenderContext,
20 },
21 events::name::EventName,
22 fifo_cache::FifoCache,
23 prelude::*,
24 tree::DiffModifies,
25};
26use freya_engine::prelude::{
27 Canvas,
28 Font,
29 FontEdging,
30 FontHinting,
31 FontStyle,
32 Paint,
33 PaintStyle,
34 ParagraphBuilder,
35 ParagraphStyle,
36 SkRect,
37 TextBlob,
38 TextStyle,
39};
40use rustc_hash::FxHashMap;
41use torin::prelude::{
42 Area,
43 Size2D,
44};
45
46use crate::{
47 colors::map_vt100_color,
48 handle::TerminalHandle,
49 rendering::{
50 CachedRow,
51 TextRenderer,
52 },
53};
54
55struct TerminalMeasure {
57 char_width: f32,
58 line_height: f32,
59 baseline_offset: f32,
60 font: Font,
61 font_family: String,
62 font_size: f32,
63 row_cache: RefCell<FifoCache<u64, CachedRow>>,
64}
65
66struct TerminalRenderer<'a> {
68 canvas: &'a Canvas,
69 paint: &'a mut Paint,
70 area: Area,
71 char_width: f32,
72 line_height: f32,
73 baseline_offset: f32,
74 foreground: Color,
75 background: Color,
76 selection_color: Color,
77}
78
79impl TerminalRenderer<'_> {
80 fn render_background(&mut self) {
81 self.paint.set_color(self.background);
82 self.canvas.draw_rect(
83 SkRect::new(
84 self.area.min_x(),
85 self.area.min_y(),
86 self.area.max_x(),
87 self.area.max_y(),
88 ),
89 self.paint,
90 );
91 }
92
93 fn render_selection(
94 &mut self,
95 row_idx: usize,
96 row_len: usize,
97 y: f32,
98 bounds: &(i64, usize, i64, usize),
99 ) {
100 let (start_row, start_col, end_row, end_col) = *bounds;
101 let row_i = row_idx as i64;
102
103 if row_i < start_row || row_i > end_row {
104 return;
105 }
106
107 let sel_start = if row_i == start_row { start_col } else { 0 };
108 let sel_end = if row_i == end_row {
109 end_col.min(row_len)
110 } else {
111 row_len
112 };
113
114 if sel_start < sel_end {
115 let left = self.area.min_x() + (sel_start as f32) * self.char_width;
116 let right = self.area.min_x() + (sel_end as f32) * self.char_width;
117 self.paint.set_color(self.selection_color);
118 self.canvas.draw_rect(
119 SkRect::new(left, y, right, y + self.line_height),
120 self.paint,
121 );
122 }
123 }
124
125 fn render_cell_backgrounds(&mut self, row: &[vt100::Cell], y: f32) {
126 let mut run_start: Option<(usize, Color)> = None;
127 let mut col = 0;
128 while col < row.len() {
129 let cell = &row[col];
130 if cell.is_wide_continuation() {
131 col += 1;
132 continue;
133 }
134 let cell_bg = if cell.inverse() {
135 map_vt100_color(cell.fgcolor(), self.foreground)
136 } else {
137 map_vt100_color(cell.bgcolor(), self.background)
138 };
139 let end_col = if cell.is_wide() { col + 2 } else { col + 1 };
140
141 if cell_bg != self.background {
142 match &run_start {
143 Some((_, color)) if *color == cell_bg => {}
144 Some((start, color)) => {
145 self.render_cell_background(*start, col, *color, y);
146 run_start = Some((col, cell_bg));
147 }
148 None => {
149 run_start = Some((col, cell_bg));
150 }
151 }
152 } else if let Some((start, color)) = run_start.take() {
153 self.render_cell_background(start, col, color, y);
154 }
155 col = end_col;
156 }
157 if let Some((start, color)) = run_start {
158 self.render_cell_background(start, col, color, y);
159 }
160 }
161
162 fn render_cell_background(&mut self, start: usize, end: usize, color: Color, y: f32) {
163 let left = self.area.min_x() + (start as f32) * self.char_width;
164 let right = self.area.min_x() + (end as f32) * self.char_width;
165 self.paint.set_color(color);
166 self.canvas.draw_rect(
167 SkRect::new(left, y, right, y + self.line_height),
168 self.paint,
169 );
170 }
171
172 fn render_cursor(&mut self, row: &[vt100::Cell], y: f32, cursor_col: usize, font: &Font) {
173 let left = self.area.min_x() + (cursor_col as f32) * self.char_width;
174 let right = left + self.char_width.max(1.0);
175 let bottom = y + self.line_height.max(1.0);
176
177 self.paint.set_color(self.foreground);
178 self.canvas
179 .draw_rect(SkRect::new(left, y, right, bottom), self.paint);
180
181 let content = row
182 .get(cursor_col)
183 .map(|cell| {
184 if cell.has_contents() {
185 cell.contents()
186 } else {
187 " "
188 }
189 })
190 .unwrap_or(" ");
191
192 self.paint.set_color(self.background);
193 if let Some(blob) = TextBlob::from_pos_text_h(content, &[0.0], 0.0, font) {
194 self.canvas
195 .draw_text_blob(&blob, (left, y + self.baseline_offset), self.paint);
196 }
197 }
198
199 fn render_scrollbar(
200 &mut self,
201 scroll_offset: usize,
202 total_scrollback: usize,
203 rows_count: usize,
204 ) {
205 let viewport_height = self.area.height();
206 let total_rows = rows_count + total_scrollback;
207 let total_content_height = total_rows as f32 * self.line_height;
208
209 let scrollbar_height = (viewport_height * viewport_height / total_content_height).max(20.0);
210 let track_height = viewport_height - scrollbar_height;
211
212 let scroll_ratio = scroll_offset as f32 / total_scrollback as f32;
213 let thumb_y = self.area.min_y() + track_height * (1.0 - scroll_ratio);
214
215 let scrollbar_x = self.area.max_x() - 4.0;
216 let corner_radius = 2.0;
217
218 self.paint.set_anti_alias(true);
219 self.paint.set_color(Color::from_argb(50, 0, 0, 0));
220 self.canvas.draw_round_rect(
221 SkRect::new(
222 scrollbar_x,
223 self.area.min_y(),
224 self.area.max_x(),
225 self.area.max_y(),
226 ),
227 corner_radius,
228 corner_radius,
229 self.paint,
230 );
231
232 self.paint.set_color(Color::from_argb(60, 255, 255, 255));
233 self.canvas.draw_round_rect(
234 SkRect::new(
235 scrollbar_x,
236 thumb_y,
237 self.area.max_x(),
238 thumb_y + scrollbar_height,
239 ),
240 corner_radius,
241 corner_radius,
242 self.paint,
243 );
244 }
245}
246
247#[derive(Clone)]
248pub struct Terminal {
249 handle: TerminalHandle,
250 layout_data: LayoutData,
251 accessibility: AccessibilityData,
252 font_family: String,
253 font_size: f32,
254 foreground: Color,
255 background: Color,
256 selection_color: Color,
257 on_measured: Option<EventHandler<(f32, f32)>>,
258 event_handlers: FxHashMap<EventName, EventHandlerType>,
259}
260
261impl PartialEq for Terminal {
262 fn eq(&self, other: &Self) -> bool {
263 self.handle == other.handle
264 && self.font_size == other.font_size
265 && self.font_family == other.font_family
266 && self.foreground == other.foreground
267 && self.background == other.background
268 && self.event_handlers.len() == other.event_handlers.len()
269 }
270}
271
272impl Terminal {
273 pub fn new(handle: TerminalHandle) -> Self {
274 let mut accessibility = AccessibilityData::default();
275 accessibility.builder.set_role(AccessibilityRole::Terminal);
276 Self {
277 handle,
278 layout_data: Default::default(),
279 accessibility,
280 font_family: "Cascadia Code".to_string(),
281 font_size: 14.,
282 foreground: (220, 220, 220).into(),
283 background: (10, 10, 10).into(),
284 selection_color: (60, 179, 214, 0.3).into(),
285 on_measured: None,
286 event_handlers: FxHashMap::default(),
287 }
288 }
289
290 pub fn selection_color(mut self, selection_color: impl Into<Color>) -> Self {
291 self.selection_color = selection_color.into();
292 self
293 }
294
295 pub fn on_measured(mut self, callback: impl Into<EventHandler<(f32, f32)>>) -> Self {
296 self.on_measured = Some(callback.into());
297 self
298 }
299
300 pub fn font_family(mut self, font_family: impl Into<String>) -> Self {
301 self.font_family = font_family.into();
302 self
303 }
304
305 pub fn font_size(mut self, font_size: f32) -> Self {
306 self.font_size = font_size;
307 self
308 }
309
310 pub fn foreground(mut self, foreground: impl Into<Color>) -> Self {
311 self.foreground = foreground.into();
312 self
313 }
314
315 pub fn background(mut self, background: impl Into<Color>) -> Self {
316 self.background = background.into();
317 self
318 }
319}
320
321impl EventHandlersExt for Terminal {
322 fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
323 &mut self.event_handlers
324 }
325}
326
327impl LayoutExt for Terminal {
328 fn get_layout(&mut self) -> &mut LayoutData {
329 &mut self.layout_data
330 }
331}
332
333impl AccessibilityExt for Terminal {
334 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
335 &mut self.accessibility
336 }
337}
338
339impl ElementExt for Terminal {
340 fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
341 let Some(terminal) = (other.as_ref() as &dyn Any).downcast_ref::<Terminal>() else {
342 return DiffModifies::all();
343 };
344
345 let mut diff = DiffModifies::empty();
346
347 if self.font_size != terminal.font_size
348 || self.font_family != terminal.font_family
349 || self.handle != terminal.handle
350 || self.event_handlers.len() != terminal.event_handlers.len()
351 {
352 diff.insert(DiffModifies::STYLE);
353 diff.insert(DiffModifies::LAYOUT);
354 }
355
356 if self.foreground != terminal.foreground
357 || self.background != terminal.background
358 || self.selection_color != terminal.selection_color
359 {
360 diff.insert(DiffModifies::STYLE);
361 }
362
363 if self.accessibility != terminal.accessibility {
364 diff.insert(DiffModifies::ACCESSIBILITY);
365 }
366
367 diff
368 }
369
370 fn layout(&'_ self) -> Cow<'_, LayoutData> {
371 Cow::Borrowed(&self.layout_data)
372 }
373
374 fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
375 Cow::Borrowed(&self.accessibility)
376 }
377
378 fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
379 Some(Cow::Borrowed(&self.event_handlers))
380 }
381
382 fn should_hook_measurement(&self) -> bool {
383 true
384 }
385
386 fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
387 let mut builder =
389 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
390
391 let mut style = TextStyle::new();
392 style.set_font_size(self.font_size);
393 style.set_font_families(&[self.font_family.as_str()]);
394 builder.push_style(&style);
395 builder.add_text("W");
396
397 let mut paragraph = builder.build();
398 paragraph.layout(f32::MAX);
399 let mut line_height = paragraph.height();
400 if line_height <= 0.0 || line_height.is_nan() {
401 line_height = (self.font_size * 1.2).max(1.0);
402 }
403 let char_width = paragraph.max_intrinsic_width();
404
405 let mut height = context.area_size.height;
406 if height <= 0.0 {
407 height = (line_height * 24.0).max(200.0);
408 }
409
410 let target_cols = if char_width > 0.0 {
411 (context.area_size.width / char_width).floor() as u16
412 } else {
413 0
414 }
415 .max(1);
416 let target_rows = if line_height > 0.0 {
417 (height / line_height).floor() as u16
418 } else {
419 0
420 }
421 .max(1);
422
423 self.handle.resize(target_rows, target_cols);
424
425 if let Some(on_measured) = &self.on_measured {
426 on_measured.call((char_width, line_height));
427 }
428
429 let typeface = context
430 .font_collection
431 .find_typefaces(&[&self.font_family], FontStyle::default())
432 .into_iter()
433 .next()
434 .expect("Terminal font family not found");
435
436 let mut font = Font::from_typeface(typeface, self.font_size);
437 font.set_subpixel(true);
438 font.set_edging(FontEdging::SubpixelAntiAlias);
439 font.set_hinting(match self.font_size as u32 {
440 0..=6 => FontHinting::Full,
441 7..=12 => FontHinting::Normal,
442 13..=24 => FontHinting::Slight,
443 _ => FontHinting::None,
444 });
445
446 let (_, metrics) = font.metrics();
447 let baseline_offset = -metrics.ascent;
448
449 Some((
450 Size2D::new(context.area_size.width.max(100.0), height),
451 Rc::new(TerminalMeasure {
452 char_width,
453 line_height,
454 baseline_offset,
455 font,
456 font_family: self.font_family.clone(),
457 font_size: self.font_size,
458 row_cache: RefCell::new(FifoCache::new()),
459 }),
460 ))
461 }
462
463 fn render(&self, context: RenderContext) {
464 let area = context.layout_node.visible_area();
465 let measure = context
466 .layout_node
467 .data
468 .as_ref()
469 .unwrap()
470 .downcast_ref::<TerminalMeasure>()
471 .unwrap();
472
473 let font = &measure.font;
474 let baseline_offset = measure.baseline_offset;
475 let buffer = self.handle.read_buffer();
476
477 let mut paint = Paint::default();
478 paint.set_anti_alias(true);
479 paint.set_style(PaintStyle::Fill);
480
481 let mut renderer = TerminalRenderer {
482 canvas: context.canvas,
483 paint: &mut paint,
484 area,
485 char_width: measure.char_width,
486 line_height: measure.line_height,
487 baseline_offset,
488 foreground: self.foreground,
489 background: self.background,
490 selection_color: self.selection_color,
491 };
492
493 renderer.render_background();
494
495 let selection_bounds = buffer.selection.as_ref().and_then(|sel| {
496 if sel.is_empty() {
497 None
498 } else {
499 Some(sel.display_positions(buffer.scroll_offset))
500 }
501 });
502
503 let mut y = area.min_y();
504 for (row_idx, row) in buffer.rows.iter().enumerate() {
505 if y + measure.line_height > area.max_y() {
506 break;
507 }
508
509 if let Some(bounds) = &selection_bounds {
510 renderer.render_selection(row_idx, row.len(), y, bounds);
511 }
512
513 renderer.render_cell_backgrounds(row, y);
514
515 y += measure.line_height;
516 }
517
518 {
519 let mut text_renderer = TextRenderer {
520 canvas: context.canvas,
521 font,
522 font_collection: context.font_collection,
523 paint: renderer.paint,
524 row_cache: &mut measure.row_cache.borrow_mut(),
525 area_min_x: area.min_x(),
526 char_width: measure.char_width,
527 line_height: measure.line_height,
528 baseline_offset,
529 foreground: self.foreground,
530 background: self.background,
531 font_family: &measure.font_family,
532 font_size: measure.font_size,
533 };
534 text_renderer.render_text(&buffer.rows, area.min_y(), area.max_y());
535 }
536
537 if buffer.scroll_offset == 0
538 && buffer.cursor_visible
539 && let Some(row) = buffer.rows.get(buffer.cursor_row)
540 {
541 let cursor_y = area.min_y() + (buffer.cursor_row as f32) * measure.line_height;
542 if cursor_y + measure.line_height <= area.max_y() {
543 renderer.render_cursor(row, cursor_y, buffer.cursor_col, font);
544 }
545 }
546
547 if buffer.total_scrollback > 0 {
548 renderer.render_scrollbar(
549 buffer.scroll_offset,
550 buffer.total_scrollback,
551 buffer.rows_count,
552 );
553 }
554 }
555}
556
557impl From<Terminal> for Element {
558 fn from(value: Terminal) -> Self {
559 Element::Element {
560 key: DiffKey::None,
561 element: Rc::new(value),
562 elements: Vec::new(),
563 }
564 }
565}