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
55struct 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 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 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}