freya_terminal/
element.rs

1use std::{
2    any::Any,
3    borrow::Cow,
4    cell::RefCell,
5    hash::{
6        Hash,
7        Hasher,
8    },
9    rc::Rc,
10};
11
12use freya_core::{
13    data::{
14        AccessibilityData,
15        LayoutData,
16    },
17    diff_key::DiffKey,
18    element::{
19        Element,
20        ElementExt,
21        EventHandlerType,
22    },
23    events::name::EventName,
24    fifo_cache::FifoCache,
25    prelude::*,
26    tree::DiffModifies,
27};
28use freya_engine::prelude::{
29    Paint,
30    PaintStyle,
31    ParagraphBuilder,
32    ParagraphStyle,
33    SkParagraph,
34    SkRect,
35    TextStyle,
36};
37use rustc_hash::{
38    FxHashMap,
39    FxHasher,
40};
41
42use crate::{
43    colors::map_vt100_color,
44    handle::TerminalHandle,
45};
46
47/// Internal terminal rendering element
48#[derive(Clone)]
49pub struct Terminal {
50    handle: TerminalHandle,
51    layout_data: LayoutData,
52    accessibility: AccessibilityData,
53    font_family: String,
54    font_size: f32,
55    foreground: Color,
56    background: Color,
57    selection_color: Color,
58    on_measured: Option<EventHandler<(f32, f32)>>,
59    event_handlers: FxHashMap<EventName, EventHandlerType>,
60}
61
62impl PartialEq for Terminal {
63    fn eq(&self, other: &Self) -> bool {
64        self.handle == other.handle
65            && self.font_size == other.font_size
66            && self.font_family == other.font_family
67            && self.foreground == other.foreground
68            && self.background == other.background
69            && self.event_handlers.len() == other.event_handlers.len()
70    }
71}
72
73impl Terminal {
74    pub fn new(handle: TerminalHandle) -> Self {
75        let mut accessibility = AccessibilityData::default();
76        accessibility.builder.set_role(AccessibilityRole::Terminal);
77        Self {
78            handle,
79            layout_data: Default::default(),
80            accessibility,
81            font_family: "Cascadia Code".to_string(),
82            font_size: 14.,
83            foreground: (220, 220, 220).into(),
84            background: (10, 10, 10).into(),
85            selection_color: (60, 179, 214, 0.3).into(),
86            on_measured: None,
87            event_handlers: FxHashMap::default(),
88        }
89    }
90
91    /// Set the selection highlight color
92    pub fn selection_color(mut self, selection_color: impl Into<Color>) -> Self {
93        self.selection_color = selection_color.into();
94        self
95    }
96
97    /// Set callback for when dimensions are measured (char_width, line_height)
98    pub fn on_measured(mut self, callback: impl Into<EventHandler<(f32, f32)>>) -> Self {
99        self.on_measured = Some(callback.into());
100        self
101    }
102
103    pub fn font_family(mut self, font_family: impl Into<String>) -> Self {
104        self.font_family = font_family.into();
105        self
106    }
107
108    pub fn font_size(mut self, font_size: f32) -> Self {
109        self.font_size = font_size;
110        self
111    }
112
113    pub fn foreground(mut self, foreground: impl Into<Color>) -> Self {
114        self.foreground = foreground.into();
115        self
116    }
117
118    pub fn background(mut self, background: impl Into<Color>) -> Self {
119        self.background = background.into();
120        self
121    }
122}
123
124impl EventHandlersExt for Terminal {
125    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
126        &mut self.event_handlers
127    }
128}
129
130impl LayoutExt for Terminal {
131    fn get_layout(&mut self) -> &mut LayoutData {
132        &mut self.layout_data
133    }
134}
135
136impl AccessibilityExt for Terminal {
137    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
138        &mut self.accessibility
139    }
140}
141
142impl ElementExt for Terminal {
143    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
144        let Some(terminal) = (other.as_ref() as &dyn Any).downcast_ref::<Terminal>() else {
145            return DiffModifies::all();
146        };
147
148        let mut diff = DiffModifies::empty();
149
150        if self.font_size != terminal.font_size
151            || self.font_family != terminal.font_family
152            || self.handle != terminal.handle
153            || self.event_handlers.len() != terminal.event_handlers.len()
154        {
155            diff.insert(DiffModifies::STYLE);
156            diff.insert(DiffModifies::LAYOUT);
157        }
158
159        if self.background != terminal.foreground
160            || self.selection_color != terminal.selection_color
161        {
162            diff.insert(DiffModifies::STYLE);
163        }
164
165        if self.accessibility != terminal.accessibility {
166            diff.insert(DiffModifies::ACCESSIBILITY);
167        }
168
169        diff
170    }
171
172    fn layout(&'_ self) -> Cow<'_, LayoutData> {
173        Cow::Borrowed(&self.layout_data)
174    }
175
176    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
177        Cow::Borrowed(&self.accessibility)
178    }
179
180    fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
181        Some(Cow::Borrowed(&self.event_handlers))
182    }
183
184    fn should_hook_measurement(&self) -> bool {
185        true
186    }
187
188    fn measure(
189        &self,
190        context: freya_core::element::LayoutContext,
191    ) -> Option<(torin::prelude::Size2D, Rc<dyn Any>)> {
192        let mut measure_builder =
193            ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
194        let mut text_style = TextStyle::new();
195        text_style.set_font_size(self.font_size);
196        text_style.set_font_families(&[self.font_family.as_str()]);
197        measure_builder.push_style(&text_style);
198        measure_builder.add_text("W");
199        let mut measure_paragraph = measure_builder.build();
200        measure_paragraph.layout(f32::MAX);
201        let mut line_height = measure_paragraph.height();
202        if line_height <= 0.0 || line_height.is_nan() {
203            line_height = (self.font_size * 1.2).max(1.0);
204        }
205
206        let mut height = context.area_size.height;
207        if height <= 0.0 {
208            height = (line_height * 24.0).max(200.0);
209        }
210
211        let char_width = measure_paragraph.max_intrinsic_width();
212        let mut target_cols = if char_width > 0.0 {
213            (context.area_size.width / char_width).floor() as u16
214        } else {
215            1
216        };
217        if target_cols == 0 {
218            target_cols = 1;
219        }
220        let mut target_rows = if line_height > 0.0 {
221            (height / line_height).floor() as u16
222        } else {
223            1
224        };
225        if target_rows == 0 {
226            target_rows = 1;
227        }
228
229        self.handle.resize(target_rows, target_cols);
230
231        // Store dimensions and notify callback
232        if let Some(on_measured) = &self.on_measured {
233            on_measured.call((char_width, line_height));
234        }
235
236        Some((
237            torin::prelude::Size2D::new(context.area_size.width.max(100.0), height),
238            Rc::new(RefCell::new(FifoCache::<u64, Rc<SkParagraph>>::new())),
239        ))
240    }
241
242    fn render(&self, context: freya_core::element::RenderContext) {
243        let area = context.layout_node.visible_area();
244        let cache = context
245            .layout_node
246            .data
247            .as_ref()
248            .unwrap()
249            .downcast_ref::<RefCell<FifoCache<u64, Rc<SkParagraph>>>>()
250            .unwrap();
251
252        let buffer = self.handle.read_buffer();
253
254        let mut paint = Paint::default();
255        paint.set_style(PaintStyle::Fill);
256        paint.set_color(self.background);
257        context.canvas.draw_rect(
258            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
259            &paint,
260        );
261
262        let mut text_style = TextStyle::new();
263        text_style.set_color(self.foreground);
264        text_style.set_font_families(&[self.font_family.as_str()]);
265        text_style.set_font_size(self.font_size);
266
267        let mut measure_builder =
268            ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
269        measure_builder.push_style(&text_style);
270        measure_builder.add_text("W");
271        let mut measure_paragraph = measure_builder.build();
272        measure_paragraph.layout(f32::MAX);
273        let char_width = measure_paragraph.max_intrinsic_width();
274        let mut line_height = measure_paragraph.height();
275        if line_height <= 0.0 || line_height.is_nan() {
276            line_height = (self.font_size * 1.2).max(1.0);
277        }
278
279        let mut y = area.min_y();
280
281        for (row_idx, row) in buffer.rows.iter().enumerate() {
282            if y + line_height > area.max_y() {
283                break;
284            }
285
286            if let Some(selection) = &buffer.selection {
287                let (display_start, start_col, display_end, end_col) =
288                    selection.display_positions(buffer.scroll_offset);
289                let row_i = row_idx as i64;
290
291                if !selection.is_empty() && row_i >= display_start && row_i <= display_end {
292                    let sel_start_col = if row_i == display_start { start_col } else { 0 };
293                    let sel_end_col = if row_i == display_end {
294                        end_col
295                    } else {
296                        row.len()
297                    };
298
299                    for col_idx in sel_start_col..sel_end_col.min(row.len()) {
300                        let left = area.min_x() + (col_idx as f32) * char_width;
301                        let top = y;
302                        let right = left + char_width;
303                        let bottom = top + line_height;
304
305                        let mut sel_paint = Paint::default();
306                        sel_paint.set_style(PaintStyle::Fill);
307                        sel_paint.set_color(self.selection_color);
308                        context
309                            .canvas
310                            .draw_rect(SkRect::new(left, top, right, bottom), &sel_paint);
311                    }
312                }
313            }
314
315            for (col_idx, cell) in row.iter().enumerate() {
316                if cell.is_wide_continuation() {
317                    continue;
318                }
319                let cell_bg = map_vt100_color(cell.bgcolor(), self.background);
320                if cell_bg != self.background {
321                    let left = area.min_x() + (col_idx as f32) * char_width;
322                    let top = y;
323                    let cell_width = if cell.is_wide() {
324                        char_width * 2.0
325                    } else {
326                        char_width
327                    };
328                    let right = left + cell_width;
329                    let bottom = top + line_height;
330
331                    let mut bg_paint = Paint::default();
332                    bg_paint.set_style(PaintStyle::Fill);
333                    bg_paint.set_color(cell_bg);
334                    context
335                        .canvas
336                        .draw_rect(SkRect::new(left, top, right, bottom), &bg_paint);
337                }
338            }
339
340            let mut state = FxHasher::default();
341            for cell in row.iter() {
342                if cell.is_wide_continuation() {
343                    continue;
344                }
345                let color = map_vt100_color(cell.fgcolor(), self.foreground);
346                cell.contents().hash(&mut state);
347                color.hash(&mut state);
348            }
349
350            let key = state.finish();
351            if let Some(paragraph) = cache.borrow().get(&key) {
352                paragraph.paint(context.canvas, (area.min_x(), y));
353            } else {
354                let mut builder = ParagraphBuilder::new(
355                    &ParagraphStyle::default(),
356                    context.font_collection.clone(),
357                );
358                for cell in row.iter() {
359                    if cell.is_wide_continuation() {
360                        continue;
361                    }
362                    let text = if cell.has_contents() {
363                        cell.contents()
364                    } else {
365                        " "
366                    };
367                    let mut cell_style = text_style.clone();
368                    cell_style.set_color(map_vt100_color(cell.fgcolor(), self.foreground));
369                    builder.push_style(&cell_style);
370                    builder.add_text(text);
371                }
372                let mut paragraph = builder.build();
373                paragraph.layout(f32::MAX);
374                paragraph.paint(context.canvas, (area.min_x(), y));
375                cache.borrow_mut().insert(key, Rc::new(paragraph));
376            }
377
378            if row_idx == buffer.cursor_row && buffer.scroll_offset == 0 {
379                let cursor_idx = buffer.cursor_col;
380                let left = area.min_x() + (cursor_idx as f32) * char_width;
381                let top = y;
382                let right = left + char_width.max(1.0);
383                let bottom = top + line_height.max(1.0);
384
385                let mut cursor_paint = Paint::default();
386                cursor_paint.set_style(PaintStyle::Fill);
387                cursor_paint.set_color(self.foreground);
388                context
389                    .canvas
390                    .draw_rect(SkRect::new(left, top, right, bottom), &cursor_paint);
391
392                let content = row
393                    .get(cursor_idx)
394                    .map(|cell| {
395                        if cell.has_contents() {
396                            cell.contents()
397                        } else {
398                            " "
399                        }
400                    })
401                    .unwrap_or(" ");
402
403                let mut fg_text_style = text_style.clone();
404                fg_text_style.set_color(self.background);
405                let mut fg_builder = ParagraphBuilder::new(
406                    &ParagraphStyle::default(),
407                    context.font_collection.clone(),
408                );
409                fg_builder.push_style(&fg_text_style);
410                fg_builder.add_text(content);
411                let mut fg_paragraph = fg_builder.build();
412                fg_paragraph.layout((right - left).max(1.0));
413                fg_paragraph.paint(context.canvas, (left, top));
414            }
415
416            y += line_height;
417        }
418
419        // Scroll indicator
420        if buffer.total_scrollback > 0 {
421            let viewport_height = area.height();
422            let total_rows = buffer.rows_count + buffer.total_scrollback;
423            let total_content_height = total_rows as f32 * line_height;
424
425            let scrollbar_height =
426                (viewport_height * viewport_height / total_content_height).max(20.0);
427            let track_height = viewport_height - scrollbar_height;
428
429            let scroll_ratio = if buffer.total_scrollback > 0 {
430                buffer.scroll_offset as f32 / buffer.total_scrollback as f32
431            } else {
432                0.0
433            };
434
435            let thumb_y_offset = track_height * (1.0 - scroll_ratio);
436
437            let scrollbar_width = 4.0;
438            let scrollbar_x = area.max_x() - scrollbar_width;
439            let scrollbar_y = area.min_y() + thumb_y_offset;
440
441            let corner_radius = 2.0;
442            let mut track_paint = Paint::default();
443            track_paint.set_anti_alias(true);
444            track_paint.set_style(PaintStyle::Fill);
445            track_paint.set_color(Color::from_argb(50, 0, 0, 0));
446            context.canvas.draw_round_rect(
447                SkRect::new(scrollbar_x, area.min_y(), area.max_x(), area.max_y()),
448                corner_radius,
449                corner_radius,
450                &track_paint,
451            );
452
453            let mut thumb_paint = Paint::default();
454            thumb_paint.set_anti_alias(true);
455            thumb_paint.set_style(PaintStyle::Fill);
456            thumb_paint.set_color(Color::from_argb(60, 255, 255, 255));
457            context.canvas.draw_round_rect(
458                SkRect::new(
459                    scrollbar_x,
460                    scrollbar_y,
461                    area.max_x(),
462                    scrollbar_y + scrollbar_height,
463                ),
464                corner_radius,
465                corner_radius,
466                &thumb_paint,
467            );
468        }
469    }
470}
471
472impl From<Terminal> for Element {
473    fn from(value: Terminal) -> Self {
474        Element::Element {
475            key: DiffKey::None,
476            element: Rc::new(value),
477            elements: Vec::new(),
478        }
479    }
480}