Skip to main content

freya_terminal/
element.rs

1use std::{
2    any::Any,
3    borrow::Cow,
4    rc::Rc,
5};
6
7use freya_core::{
8    data::{
9        AccessibilityData,
10        LayoutData,
11    },
12    diff_key::DiffKey,
13    element::{
14        Element,
15        ElementExt,
16        EventHandlers,
17        LayoutContext,
18        RenderContext,
19    },
20    prelude::*,
21    style::default_fonts::default_monospace_fonts,
22    tree::DiffModifies,
23};
24use freya_engine::prelude::{
25    Font,
26    FontEdging,
27    FontHinting,
28    FontStyle,
29    Paint,
30    PaintStyle,
31    ParagraphBuilder,
32    ParagraphStyle,
33    TextStyle,
34};
35use rio_vt::ansi::CursorShape;
36use torin::prelude::Size2D;
37
38use crate::{
39    cell::{
40        TermCell,
41        snapshot_row,
42    },
43    handle::TerminalHandle,
44    rendering::Renderer,
45};
46
47/// Cached layout measurements and font for text drawing.
48struct TerminalMeasure {
49    char_width: f32,
50    line_height: f32,
51    baseline_offset: f32,
52    font: Font,
53    font_families: Vec<Cow<'static, str>>,
54    font_size: f32,
55}
56
57#[derive(Clone)]
58pub struct Terminal {
59    handle: TerminalHandle,
60    layout_data: LayoutData,
61    accessibility: AccessibilityData,
62    font_families: Vec<Cow<'static, str>>,
63    font_size: f32,
64    foreground: Color,
65    background: Color,
66    selection_color: Color,
67    on_measured: Option<EventHandler<(f32, f32)>>,
68    event_handlers: EventHandlers,
69}
70
71impl PartialEq for Terminal {
72    fn eq(&self, other: &Self) -> bool {
73        self.handle == other.handle
74            && self.font_size == other.font_size
75            && self.font_families == other.font_families
76            && self.foreground == other.foreground
77            && self.background == other.background
78            && self.event_handlers.len() == other.event_handlers.len()
79    }
80}
81
82impl Terminal {
83    pub fn new(handle: TerminalHandle) -> Self {
84        let mut accessibility = AccessibilityData::default();
85        accessibility.builder.set_role(AccessibilityRole::Terminal);
86        Self {
87            handle,
88            layout_data: Default::default(),
89            accessibility,
90            font_families: default_monospace_fonts(),
91            font_size: 14.,
92            foreground: (220, 220, 220).into(),
93            background: (10, 10, 10).into(),
94            selection_color: (60, 179, 214, 0.3).into(),
95            on_measured: None,
96            event_handlers: EventHandlers::default(),
97        }
98    }
99
100    pub fn selection_color(mut self, selection_color: impl Into<Color>) -> Self {
101        self.selection_color = selection_color.into();
102        self
103    }
104
105    pub fn on_measured(mut self, callback: impl Into<EventHandler<(f32, f32)>>) -> Self {
106        self.on_measured = Some(callback.into());
107        self
108    }
109
110    /// Sets the preferred font family, keeping the default fonts as fallbacks.
111    pub fn font_family(mut self, font_family: impl Into<Cow<'static, str>>) -> Self {
112        self.font_families.insert(0, font_family.into());
113        self
114    }
115
116    pub fn font_size(mut self, font_size: f32) -> Self {
117        self.font_size = font_size;
118        self
119    }
120
121    pub fn foreground(mut self, foreground: impl Into<Color>) -> Self {
122        self.foreground = foreground.into();
123        self
124    }
125
126    pub fn background(mut self, background: impl Into<Color>) -> Self {
127        self.background = background.into();
128        self
129    }
130}
131
132impl EventHandlersExt for Terminal {
133    fn event_handlers_mut(&mut self) -> &mut EventHandlers {
134        &mut self.event_handlers
135    }
136}
137
138impl LayoutExt for Terminal {
139    fn layout_mut(&mut self) -> &mut LayoutData {
140        &mut self.layout_data
141    }
142}
143
144impl AccessibilityExt for Terminal {
145    fn accessibility_data_mut(&mut self) -> &mut AccessibilityData {
146        &mut self.accessibility
147    }
148}
149
150impl ElementExt for Terminal {
151    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
152        let Some(terminal) = (other.as_ref() as &dyn Any).downcast_ref::<Terminal>() else {
153            return DiffModifies::all();
154        };
155
156        let mut diff = DiffModifies::empty();
157
158        if self.font_size != terminal.font_size
159            || self.font_families != terminal.font_families
160            || self.handle != terminal.handle
161            || self.event_handlers.len() != terminal.event_handlers.len()
162        {
163            diff.insert(DiffModifies::STYLE);
164            diff.insert(DiffModifies::LAYOUT);
165        }
166
167        if self.foreground != terminal.foreground
168            || self.background != terminal.background
169            || self.selection_color != terminal.selection_color
170        {
171            diff.insert(DiffModifies::STYLE);
172        }
173
174        if self.accessibility != terminal.accessibility {
175            diff.insert(DiffModifies::ACCESSIBILITY);
176        }
177
178        diff
179    }
180
181    fn layout(&'_ self) -> Cow<'_, LayoutData> {
182        Cow::Borrowed(&self.layout_data)
183    }
184
185    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
186        Cow::Borrowed(&self.accessibility)
187    }
188
189    fn events_handlers(&'_ self) -> Option<Cow<'_, EventHandlers>> {
190        Some(Cow::Borrowed(&self.event_handlers))
191    }
192
193    fn should_hook_measurement(&self) -> bool {
194        true
195    }
196
197    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
198        let scaled_font_size = self.font_size * context.scale_factor as f32;
199
200        // Measure char width and line height using a reference glyph
201        let mut builder =
202            ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
203
204        let mut style = TextStyle::new();
205        style.set_font_size(scaled_font_size);
206        style.set_font_families(&self.font_families);
207        builder.push_style(&style);
208        builder.add_text("W");
209
210        let mut paragraph = builder.build();
211        paragraph.layout(f32::MAX);
212        let mut line_height = paragraph.height();
213        if line_height <= 0.0 || line_height.is_nan() {
214            line_height = (self.font_size * 1.2).max(1.0);
215        }
216        let char_width = paragraph.max_intrinsic_width();
217
218        let mut height = context.area_size.height;
219        if height <= 0.0 {
220            height = (line_height * 24.0).max(200.0);
221        }
222
223        let target_cols = if char_width > 0.0 {
224            (context.area_size.width / char_width).floor() as u16
225        } else {
226            0
227        }
228        .max(1);
229        let target_rows = if line_height > 0.0 {
230            (height / line_height).floor() as u16
231        } else {
232            0
233        }
234        .max(1);
235
236        self.handle.resize(target_rows, target_cols);
237
238        if let Some(on_measured) = &self.on_measured {
239            let scale = context.scale_factor as f32;
240            on_measured.call((char_width / scale, line_height / scale));
241        }
242
243        let typeface = context
244            .font_collection
245            .find_typefaces(&self.font_families, FontStyle::default())
246            .into_iter()
247            .next()
248            .expect("Terminal font family not found");
249
250        let mut font = Font::from_typeface(typeface, scaled_font_size);
251        font.set_subpixel(true);
252        font.set_edging(FontEdging::SubpixelAntiAlias);
253        font.set_hinting(match scaled_font_size as u32 {
254            0..=6 => FontHinting::Full,
255            7..=12 => FontHinting::Normal,
256            13..=24 => FontHinting::Slight,
257            _ => FontHinting::None,
258        });
259
260        let baseline_offset = paragraph.alphabetic_baseline();
261
262        Some((
263            Size2D::new(context.area_size.width.max(100.0), height),
264            Rc::new(TerminalMeasure {
265                char_width,
266                line_height,
267                baseline_offset,
268                font,
269                font_families: self.font_families.clone(),
270                font_size: scaled_font_size,
271            }),
272        ))
273    }
274
275    fn render(&self, context: RenderContext) {
276        let area = context.layout_node.visible_area();
277        let measure = context
278            .layout_node
279            .data
280            .as_ref()
281            .unwrap()
282            .downcast_ref::<TerminalMeasure>()
283            .unwrap();
284
285        let term = self.handle.term();
286        let screen_lines = term.screen_lines();
287        let display_offset = term.display_offset();
288        let total_scrollback = term.history_size();
289
290        let mut paint = Paint::default();
291        paint.set_anti_alias(true);
292        paint.set_style(PaintStyle::Fill);
293
294        let mut renderer = Renderer {
295            canvas: context.canvas,
296            paint: &mut paint,
297            font: &measure.font,
298            font_collection: context.font_collection,
299            row_cache: &mut self.handle.row_cache.borrow_mut(),
300            area,
301            char_width: measure.char_width,
302            line_height: measure.line_height,
303            baseline_offset: measure.baseline_offset,
304            foreground: self.foreground,
305            background: self.background,
306            selection_color: self.selection_color,
307            font_families: &measure.font_families,
308            font_size: measure.font_size,
309            selection: term.selection.as_ref().and_then(|s| s.to_range(&*term)),
310            display_offset,
311        };
312
313        renderer.render_background();
314
315        let mut row: Vec<TermCell> = Vec::with_capacity(term.columns());
316        let mut y = area.min_y();
317        for row_idx in 0..screen_lines {
318            if y + measure.line_height > area.max_y() {
319                break;
320            }
321            snapshot_row(&term, row_idx, &mut row);
322            renderer.render_row(row_idx, &row, y);
323            y += measure.line_height;
324        }
325
326        let cursor = term.cursor();
327        if cursor.content != CursorShape::Hidden {
328            let cursor_line = cursor.pos.row.0.max(0) as usize;
329            let cursor_y = area.min_y() + (cursor_line as f32) * measure.line_height;
330            if cursor_line < screen_lines && cursor_y + measure.line_height <= area.max_y() {
331                snapshot_row(&term, cursor_line, &mut row);
332                if let Some(cursor_cell) = row.get(cursor.pos.col.0) {
333                    renderer.render_cursor(cursor_cell, cursor_y, cursor.pos.col.0);
334                }
335            }
336        }
337
338        if total_scrollback > 0 {
339            renderer.render_scrollbar(display_offset, total_scrollback, screen_lines);
340        }
341    }
342}
343
344impl From<Terminal> for Element {
345    fn from(value: Terminal) -> Self {
346        Element::Element {
347            key: DiffKey::None,
348            element: Rc::new(value),
349            elements: Vec::new(),
350        }
351    }
352}