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<'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        // Measure char width and line height using a reference glyph
388        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}