Skip to main content

freya_terminal/
element.rs

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