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