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