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    foreground: Color,
52    background: 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.foreground == other.foreground
64            && self.background == other.background
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            foreground: (220, 220, 220).into(),
77            background: (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.foreground = foreground.into();
108        self
109    }
110
111    pub fn background(mut self, background: impl Into<Color>) -> Self {
112        self.background = 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(terminal) = (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 != terminal.font_size
138            || self.font_family != terminal.font_family
139            || self.handle != terminal.handle
140            || self.event_handlers.len() != terminal.event_handlers.len()
141        {
142            diff.insert(DiffModifies::STYLE);
143            diff.insert(DiffModifies::LAYOUT);
144        }
145
146        if self.background != terminal.foreground
147            || self.selection_color != terminal.selection_color
148        {
149            diff.insert(DiffModifies::STYLE);
150        }
151
152        diff
153    }
154
155    fn layout(&'_ self) -> Cow<'_, LayoutData> {
156        Cow::Borrowed(&self.layout_data)
157    }
158
159    fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
160        Some(Cow::Borrowed(&self.event_handlers))
161    }
162
163    fn should_hook_measurement(&self) -> bool {
164        true
165    }
166
167    fn measure(
168        &self,
169        context: freya_core::element::LayoutContext,
170    ) -> Option<(torin::prelude::Size2D, Rc<dyn Any>)> {
171        let mut measure_builder =
172            ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
173        let mut text_style = TextStyle::new();
174        text_style.set_font_size(self.font_size);
175        text_style.set_font_families(&[self.font_family.as_str()]);
176        measure_builder.push_style(&text_style);
177        measure_builder.add_text("W");
178        let mut measure_paragraph = measure_builder.build();
179        measure_paragraph.layout(f32::MAX);
180        let mut line_height = measure_paragraph.height();
181        if line_height <= 0.0 || line_height.is_nan() {
182            line_height = (self.font_size * 1.2).max(1.0);
183        }
184
185        let mut height = context.area_size.height;
186        if height <= 0.0 {
187            height = (line_height * 24.0).max(200.0);
188        }
189
190        let char_width = measure_paragraph.max_intrinsic_width();
191        let mut target_cols = if char_width > 0.0 {
192            (context.area_size.width / char_width).floor() as u16
193        } else {
194            1
195        };
196        if target_cols == 0 {
197            target_cols = 1;
198        }
199        let mut target_rows = if line_height > 0.0 {
200            (height / line_height).floor() as u16
201        } else {
202            1
203        };
204        if target_rows == 0 {
205            target_rows = 1;
206        }
207
208        self.handle.resize(target_rows, target_cols);
209
210        // Store dimensions and notify callback
211        if let Some(on_measured) = &self.on_measured {
212            on_measured.call((char_width, line_height));
213        }
214
215        Some((
216            torin::prelude::Size2D::new(context.area_size.width.max(100.0), height),
217            Rc::new(RefCell::new(FifoCache::<u64, Rc<SkParagraph>>::new())),
218        ))
219    }
220
221    fn render(&self, context: freya_core::element::RenderContext) {
222        let area = context.layout_node.visible_area();
223        let cache = context
224            .layout_node
225            .data
226            .as_ref()
227            .unwrap()
228            .downcast_ref::<RefCell<FifoCache<u64, Rc<SkParagraph>>>>()
229            .unwrap();
230
231        let buffer = self.handle.read_buffer();
232
233        let mut paint = Paint::default();
234        paint.set_style(PaintStyle::Fill);
235        paint.set_color(self.background);
236        context.canvas.draw_rect(
237            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
238            &paint,
239        );
240
241        let mut text_style = TextStyle::new();
242        text_style.set_color(self.foreground);
243        text_style.set_font_families(&[self.font_family.as_str()]);
244        text_style.set_font_size(self.font_size);
245
246        let mut measure_builder =
247            ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
248        measure_builder.push_style(&text_style);
249        measure_builder.add_text("W");
250        let mut measure_paragraph = measure_builder.build();
251        measure_paragraph.layout(f32::MAX);
252        let char_width = measure_paragraph.max_intrinsic_width();
253        let mut line_height = measure_paragraph.height();
254        if line_height <= 0.0 || line_height.is_nan() {
255            line_height = (self.font_size * 1.2).max(1.0);
256        }
257
258        let mut y = area.min_y();
259
260        for (row_idx, row) in buffer.rows.iter().enumerate() {
261            if y + line_height > area.max_y() {
262                break;
263            }
264
265            if let Some(selection) = &buffer.selection {
266                let (display_start, start_col, display_end, end_col) =
267                    selection.display_positions(buffer.scroll_offset);
268                let row_i = row_idx as i64;
269
270                if !selection.is_empty() && row_i >= display_start && row_i <= display_end {
271                    let sel_start_col = if row_i == display_start { start_col } else { 0 };
272                    let sel_end_col = if row_i == display_end {
273                        end_col
274                    } else {
275                        row.len()
276                    };
277
278                    for col_idx in sel_start_col..sel_end_col.min(row.len()) {
279                        let left = area.min_x() + (col_idx as f32) * char_width;
280                        let top = y;
281                        let right = left + char_width;
282                        let bottom = top + line_height;
283
284                        let mut sel_paint = Paint::default();
285                        sel_paint.set_style(PaintStyle::Fill);
286                        sel_paint.set_color(self.selection_color);
287                        context
288                            .canvas
289                            .draw_rect(SkRect::new(left, top, right, bottom), &sel_paint);
290                    }
291                }
292            }
293
294            for (col_idx, cell) in row.iter().enumerate() {
295                if cell.is_wide_continuation() {
296                    continue;
297                }
298                let cell_bg = map_vt100_color(cell.bgcolor(), self.background);
299                if cell_bg != self.background {
300                    let left = area.min_x() + (col_idx as f32) * char_width;
301                    let top = y;
302                    let cell_width = if cell.is_wide() {
303                        char_width * 2.0
304                    } else {
305                        char_width
306                    };
307                    let right = left + cell_width;
308                    let bottom = top + line_height;
309
310                    let mut bg_paint = Paint::default();
311                    bg_paint.set_style(PaintStyle::Fill);
312                    bg_paint.set_color(cell_bg);
313                    context
314                        .canvas
315                        .draw_rect(SkRect::new(left, top, right, bottom), &bg_paint);
316                }
317            }
318
319            let mut state = FxHasher::default();
320            for cell in row.iter() {
321                if cell.is_wide_continuation() {
322                    continue;
323                }
324                let color = map_vt100_color(cell.fgcolor(), self.foreground);
325                cell.contents().hash(&mut state);
326                color.hash(&mut state);
327            }
328
329            let key = state.finish();
330            if let Some(paragraph) = cache.borrow().get(&key) {
331                paragraph.paint(context.canvas, (area.min_x(), y));
332            } else {
333                let mut builder = ParagraphBuilder::new(
334                    &ParagraphStyle::default(),
335                    context.font_collection.clone(),
336                );
337                for cell in row.iter() {
338                    if cell.is_wide_continuation() {
339                        continue;
340                    }
341                    let text = if cell.has_contents() {
342                        cell.contents()
343                    } else {
344                        " "
345                    };
346                    let mut cell_style = text_style.clone();
347                    cell_style.set_color(map_vt100_color(cell.fgcolor(), self.foreground));
348                    builder.push_style(&cell_style);
349                    builder.add_text(text);
350                }
351                let mut paragraph = builder.build();
352                paragraph.layout(f32::MAX);
353                paragraph.paint(context.canvas, (area.min_x(), y));
354                cache.borrow_mut().insert(key, Rc::new(paragraph));
355            }
356
357            if row_idx == buffer.cursor_row && buffer.scroll_offset == 0 {
358                let cursor_idx = buffer.cursor_col;
359                let left = area.min_x() + (cursor_idx as f32) * char_width;
360                let top = y;
361                let right = left + char_width.max(1.0);
362                let bottom = top + line_height.max(1.0);
363
364                let mut cursor_paint = Paint::default();
365                cursor_paint.set_style(PaintStyle::Fill);
366                cursor_paint.set_color(self.foreground);
367                context
368                    .canvas
369                    .draw_rect(SkRect::new(left, top, right, bottom), &cursor_paint);
370
371                let content = row
372                    .get(cursor_idx)
373                    .map(|cell| {
374                        if cell.has_contents() {
375                            cell.contents()
376                        } else {
377                            " "
378                        }
379                    })
380                    .unwrap_or(" ");
381
382                let mut fg_text_style = text_style.clone();
383                fg_text_style.set_color(self.background);
384                let mut fg_builder = ParagraphBuilder::new(
385                    &ParagraphStyle::default(),
386                    context.font_collection.clone(),
387                );
388                fg_builder.push_style(&fg_text_style);
389                fg_builder.add_text(content);
390                let mut fg_paragraph = fg_builder.build();
391                fg_paragraph.layout((right - left).max(1.0));
392                fg_paragraph.paint(context.canvas, (left, top));
393            }
394
395            y += line_height;
396        }
397
398        // Scroll indicator
399        if buffer.total_scrollback > 0 {
400            let viewport_height = area.height();
401            let total_rows = buffer.rows_count + buffer.total_scrollback;
402            let total_content_height = total_rows as f32 * line_height;
403
404            let scrollbar_height =
405                (viewport_height * viewport_height / total_content_height).max(20.0);
406            let track_height = viewport_height - scrollbar_height;
407
408            let scroll_ratio = if buffer.total_scrollback > 0 {
409                buffer.scroll_offset as f32 / buffer.total_scrollback as f32
410            } else {
411                0.0
412            };
413
414            let thumb_y_offset = track_height * (1.0 - scroll_ratio);
415
416            let scrollbar_width = 4.0;
417            let scrollbar_x = area.max_x() - scrollbar_width;
418            let scrollbar_y = area.min_y() + thumb_y_offset;
419
420            let corner_radius = 2.0;
421            let mut track_paint = Paint::default();
422            track_paint.set_anti_alias(true);
423            track_paint.set_style(PaintStyle::Fill);
424            track_paint.set_color(Color::from_argb(50, 0, 0, 0));
425            context.canvas.draw_round_rect(
426                SkRect::new(scrollbar_x, area.min_y(), area.max_x(), area.max_y()),
427                corner_radius,
428                corner_radius,
429                &track_paint,
430            );
431
432            let mut thumb_paint = Paint::default();
433            thumb_paint.set_anti_alias(true);
434            thumb_paint.set_style(PaintStyle::Fill);
435            thumb_paint.set_color(Color::from_argb(60, 255, 255, 255));
436            context.canvas.draw_round_rect(
437                SkRect::new(
438                    scrollbar_x,
439                    scrollbar_y,
440                    area.max_x(),
441                    scrollbar_y + scrollbar_height,
442                ),
443                corner_radius,
444                corner_radius,
445                &thumb_paint,
446            );
447        }
448    }
449}
450
451impl From<Terminal> for Element {
452    fn from(value: Terminal) -> Self {
453        Element::Element {
454            key: DiffKey::None,
455            element: Rc::new(value),
456            elements: Vec::new(),
457        }
458    }
459}