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