Skip to main content

freya_terminal/
element.rs

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
55/// Cached layout measurements and font for text drawing.
56struct 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
66/// Renders selection, backgrounds, cursor, and scrollbar.
67struct 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        // Measure char width and line height using a reference glyph
412        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}