freya_terminal/
element.rs

1use std::{
2    any::Any,
3    borrow::Cow,
4    rc::Rc,
5};
6
7use freya_core::{
8    data::LayoutData,
9    diff_key::DiffKey,
10    element::{
11        Element,
12        ElementExt,
13    },
14    prelude::*,
15    tree::DiffModifies,
16};
17use freya_engine::prelude::{
18    Paint,
19    PaintStyle,
20    ParagraphBuilder,
21    ParagraphStyle,
22    SkRect,
23    TextStyle,
24};
25
26use crate::{
27    colors::{
28        map_vt100_bg_color,
29        map_vt100_fg_color,
30    },
31    handle::TerminalHandle,
32};
33
34/// Internal terminal rendering element
35#[derive(Clone)]
36pub struct TerminalElement {
37    handle: TerminalHandle,
38    layout_data: LayoutData,
39    font_family: String,
40    font_size: f32,
41    fg: Color,
42    bg: Color,
43}
44
45impl PartialEq for TerminalElement {
46    fn eq(&self, other: &Self) -> bool {
47        self.handle == other.handle
48            && self.font_size == other.font_size
49            && self.font_family == other.font_family
50            && self.fg == other.fg
51            && self.bg == other.bg
52    }
53}
54
55impl TerminalElement {
56    pub(crate) fn new(handle: TerminalHandle) -> Self {
57        Self {
58            handle,
59            layout_data: Default::default(),
60            font_family: "Cascadia Code".to_string(),
61            font_size: 14.,
62            fg: (220, 220, 220).into(),
63            bg: (10, 10, 10).into(),
64        }
65    }
66
67    pub fn font_family(mut self, font_family: impl Into<String>) -> Self {
68        self.font_family = font_family.into();
69        self
70    }
71
72    pub fn font_size(mut self, font_size: f32) -> Self {
73        self.font_size = font_size;
74        self
75    }
76
77    pub fn foreground(mut self, foreground: impl Into<Color>) -> Self {
78        self.fg = foreground.into();
79        self
80    }
81
82    pub fn background(mut self, background: impl Into<Color>) -> Self {
83        self.bg = background.into();
84        self
85    }
86}
87
88impl ElementExt for TerminalElement {
89    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
90        let Some(el) = (other.as_ref() as &dyn Any).downcast_ref::<TerminalElement>() else {
91            return DiffModifies::all();
92        };
93
94        let mut diff = DiffModifies::empty();
95
96        if self.font_size != el.font_size
97            || self.font_family != el.font_family
98            || self.handle != el.handle
99        {
100            diff.insert(DiffModifies::STYLE);
101            diff.insert(DiffModifies::LAYOUT);
102        }
103
104        diff
105    }
106
107    fn layout(&'_ self) -> Cow<'_, LayoutData> {
108        Cow::Borrowed(&self.layout_data)
109    }
110
111    fn should_hook_measurement(&self) -> bool {
112        true
113    }
114
115    fn measure(
116        &self,
117        context: freya_core::element::LayoutContext,
118    ) -> Option<(torin::prelude::Size2D, Rc<dyn Any>)> {
119        let mut measure_builder =
120            ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
121        let mut text_style = TextStyle::new();
122        text_style.set_font_size(self.font_size);
123        text_style.set_font_families(&[self.font_family.as_str()]);
124        measure_builder.push_style(&text_style);
125        measure_builder.add_text("W");
126        let mut measure_paragraph = measure_builder.build();
127        measure_paragraph.layout(f32::MAX);
128        let mut line_height = measure_paragraph.height();
129        if line_height <= 0.0 || line_height.is_nan() {
130            line_height = (self.font_size * 1.2).max(1.0);
131        }
132
133        let mut height = context.area_size.height;
134        if height <= 0.0 {
135            height = (line_height * 24.0).max(200.0);
136        }
137
138        let char_width = measure_paragraph.max_intrinsic_width();
139        let mut target_cols = if char_width > 0.0 {
140            (context.area_size.width / char_width).floor() as u16
141        } else {
142            1
143        };
144        if target_cols == 0 {
145            target_cols = 1;
146        }
147        let mut target_rows = if line_height > 0.0 {
148            (height / line_height).floor() as u16
149        } else {
150            1
151        };
152        if target_rows == 0 {
153            target_rows = 1;
154        }
155
156        self.handle.resize(target_rows, target_cols);
157
158        Some((
159            torin::prelude::Size2D::new(context.area_size.width.max(100.0), height),
160            Rc::new(()),
161        ))
162    }
163
164    fn render(&self, context: freya_core::element::RenderContext) {
165        let area = context.layout_node.visible_area();
166
167        let buffer = self.handle.read_buffer();
168
169        let mut paint = Paint::default();
170        paint.set_style(PaintStyle::Fill);
171        paint.set_color(self.bg);
172        context.canvas.draw_rect(
173            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
174            &paint,
175        );
176
177        let mut text_style = TextStyle::new();
178        text_style.set_color(self.fg);
179        text_style.set_font_families(&[self.font_family.as_str()]);
180        text_style.set_font_size(self.font_size);
181
182        let mut measure_builder =
183            ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
184        measure_builder.push_style(&text_style);
185        measure_builder.add_text("W");
186        let mut measure_paragraph = measure_builder.build();
187        measure_paragraph.layout(f32::MAX);
188        let char_width = measure_paragraph.max_intrinsic_width();
189        let mut line_height = measure_paragraph.height();
190        if line_height <= 0.0 || line_height.is_nan() {
191            line_height = (self.font_size * 1.2).max(1.0);
192        }
193
194        let mut y = area.min_y();
195
196        for (row_idx, row) in buffer.rows.iter().enumerate() {
197            if y + line_height > area.max_y() {
198                break;
199            }
200
201            for (col_idx, cell) in row.iter().enumerate() {
202                if cell.is_wide_continuation() {
203                    continue;
204                }
205                let cell_bg = map_vt100_bg_color(cell.bgcolor(), self.fg, self.bg);
206                if cell_bg != self.bg {
207                    let left = area.min_x() + (col_idx as f32) * char_width;
208                    let top = y;
209                    let cell_width = if cell.is_wide() {
210                        char_width * 2.0
211                    } else {
212                        char_width
213                    };
214                    let right = left + cell_width;
215                    let bottom = top + line_height;
216
217                    let mut bg_paint = Paint::default();
218                    bg_paint.set_style(PaintStyle::Fill);
219                    bg_paint.set_color(cell_bg);
220                    context
221                        .canvas
222                        .draw_rect(SkRect::new(left, top, right, bottom), &bg_paint);
223                }
224            }
225
226            let mut builder =
227                ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
228            for cell in row.iter() {
229                if cell.is_wide_continuation() {
230                    continue;
231                }
232                let text = if cell.has_contents() {
233                    cell.contents()
234                } else {
235                    " "
236                };
237                let mut cell_style = text_style.clone();
238                cell_style.set_color(map_vt100_fg_color(cell.fgcolor(), self.fg, self.bg));
239                builder.push_style(&cell_style);
240                builder.add_text(text);
241            }
242            let mut paragraph = builder.build();
243            paragraph.layout(f32::MAX);
244            paragraph.paint(context.canvas, (area.min_x(), y));
245
246            if row_idx == buffer.cursor_row {
247                let cursor_idx = buffer.cursor_col;
248                let left = area.min_x() + (cursor_idx as f32) * char_width;
249                let top = y;
250                let right = left + char_width.max(1.0);
251                let bottom = top + line_height.max(1.0);
252
253                let mut cursor_paint = Paint::default();
254                cursor_paint.set_style(PaintStyle::Fill);
255                cursor_paint.set_color(self.fg);
256                context
257                    .canvas
258                    .draw_rect(SkRect::new(left, top, right, bottom), &cursor_paint);
259
260                let content = row
261                    .get(cursor_idx)
262                    .map(|cell| {
263                        if cell.has_contents() {
264                            cell.contents()
265                        } else {
266                            " "
267                        }
268                    })
269                    .unwrap_or(" ");
270
271                let mut fg_text_style = text_style.clone();
272                fg_text_style.set_color(self.bg);
273                let mut fg_builder = ParagraphBuilder::new(
274                    &ParagraphStyle::default(),
275                    context.font_collection.clone(),
276                );
277                fg_builder.push_style(&fg_text_style);
278                fg_builder.add_text(content);
279                let mut fg_paragraph = fg_builder.build();
280                fg_paragraph.layout((right - left).max(1.0));
281                fg_paragraph.paint(context.canvas, (left, top));
282            }
283
284            y += line_height;
285        }
286    }
287}
288
289impl From<TerminalElement> for Element {
290    fn from(value: TerminalElement) -> Self {
291        Element::Element {
292            key: DiffKey::None,
293            element: Rc::new(value),
294            elements: Vec::new(),
295        }
296    }
297}