1use std::{
2 any::Any,
3 borrow::Cow,
4 cell::RefCell,
5 hash::{
6 Hash,
7 Hasher,
8 },
9 rc::Rc,
10};
11
12use freya_core::{
13 data::{
14 AccessibilityData,
15 LayoutData,
16 },
17 diff_key::DiffKey,
18 element::{
19 Element,
20 ElementExt,
21 EventHandlerType,
22 },
23 events::name::EventName,
24 fifo_cache::FifoCache,
25 prelude::*,
26 tree::DiffModifies,
27};
28use freya_engine::prelude::{
29 Paint,
30 PaintStyle,
31 ParagraphBuilder,
32 ParagraphStyle,
33 SkParagraph,
34 SkRect,
35 TextStyle,
36};
37use rustc_hash::{
38 FxHashMap,
39 FxHasher,
40};
41
42use crate::{
43 colors::map_vt100_color,
44 handle::TerminalHandle,
45};
46
47#[derive(Clone)]
49pub struct Terminal {
50 handle: TerminalHandle,
51 layout_data: LayoutData,
52 accessibility: AccessibilityData,
53 font_family: String,
54 font_size: f32,
55 foreground: Color,
56 background: Color,
57 selection_color: Color,
58 on_measured: Option<EventHandler<(f32, f32)>>,
59 event_handlers: FxHashMap<EventName, EventHandlerType>,
60}
61
62impl PartialEq for Terminal {
63 fn eq(&self, other: &Self) -> bool {
64 self.handle == other.handle
65 && self.font_size == other.font_size
66 && self.font_family == other.font_family
67 && self.foreground == other.foreground
68 && self.background == other.background
69 && self.event_handlers.len() == other.event_handlers.len()
70 }
71}
72
73impl Terminal {
74 pub fn new(handle: TerminalHandle) -> Self {
75 let mut accessibility = AccessibilityData::default();
76 accessibility.builder.set_role(AccessibilityRole::Terminal);
77 Self {
78 handle,
79 layout_data: Default::default(),
80 accessibility,
81 font_family: "Cascadia Code".to_string(),
82 font_size: 14.,
83 foreground: (220, 220, 220).into(),
84 background: (10, 10, 10).into(),
85 selection_color: (60, 179, 214, 0.3).into(),
86 on_measured: None,
87 event_handlers: FxHashMap::default(),
88 }
89 }
90
91 pub fn selection_color(mut self, selection_color: impl Into<Color>) -> Self {
93 self.selection_color = selection_color.into();
94 self
95 }
96
97 pub fn on_measured(mut self, callback: impl Into<EventHandler<(f32, f32)>>) -> Self {
99 self.on_measured = Some(callback.into());
100 self
101 }
102
103 pub fn font_family(mut self, font_family: impl Into<String>) -> Self {
104 self.font_family = font_family.into();
105 self
106 }
107
108 pub fn font_size(mut self, font_size: f32) -> Self {
109 self.font_size = font_size;
110 self
111 }
112
113 pub fn foreground(mut self, foreground: impl Into<Color>) -> Self {
114 self.foreground = foreground.into();
115 self
116 }
117
118 pub fn background(mut self, background: impl Into<Color>) -> Self {
119 self.background = background.into();
120 self
121 }
122}
123
124impl EventHandlersExt for Terminal {
125 fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
126 &mut self.event_handlers
127 }
128}
129
130impl LayoutExt for Terminal {
131 fn get_layout(&mut self) -> &mut LayoutData {
132 &mut self.layout_data
133 }
134}
135
136impl AccessibilityExt for Terminal {
137 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
138 &mut self.accessibility
139 }
140}
141
142impl ElementExt for Terminal {
143 fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
144 let Some(terminal) = (other.as_ref() as &dyn Any).downcast_ref::<Terminal>() else {
145 return DiffModifies::all();
146 };
147
148 let mut diff = DiffModifies::empty();
149
150 if self.font_size != terminal.font_size
151 || self.font_family != terminal.font_family
152 || self.handle != terminal.handle
153 || self.event_handlers.len() != terminal.event_handlers.len()
154 {
155 diff.insert(DiffModifies::STYLE);
156 diff.insert(DiffModifies::LAYOUT);
157 }
158
159 if self.background != terminal.foreground
160 || self.selection_color != terminal.selection_color
161 {
162 diff.insert(DiffModifies::STYLE);
163 }
164
165 if self.accessibility != terminal.accessibility {
166 diff.insert(DiffModifies::ACCESSIBILITY);
167 }
168
169 diff
170 }
171
172 fn layout(&'_ self) -> Cow<'_, LayoutData> {
173 Cow::Borrowed(&self.layout_data)
174 }
175
176 fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
177 Cow::Borrowed(&self.accessibility)
178 }
179
180 fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
181 Some(Cow::Borrowed(&self.event_handlers))
182 }
183
184 fn should_hook_measurement(&self) -> bool {
185 true
186 }
187
188 fn measure(
189 &self,
190 context: freya_core::element::LayoutContext,
191 ) -> Option<(torin::prelude::Size2D, Rc<dyn Any>)> {
192 let mut measure_builder =
193 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
194 let mut text_style = TextStyle::new();
195 text_style.set_font_size(self.font_size);
196 text_style.set_font_families(&[self.font_family.as_str()]);
197 measure_builder.push_style(&text_style);
198 measure_builder.add_text("W");
199 let mut measure_paragraph = measure_builder.build();
200 measure_paragraph.layout(f32::MAX);
201 let mut line_height = measure_paragraph.height();
202 if line_height <= 0.0 || line_height.is_nan() {
203 line_height = (self.font_size * 1.2).max(1.0);
204 }
205
206 let mut height = context.area_size.height;
207 if height <= 0.0 {
208 height = (line_height * 24.0).max(200.0);
209 }
210
211 let char_width = measure_paragraph.max_intrinsic_width();
212 let mut target_cols = if char_width > 0.0 {
213 (context.area_size.width / char_width).floor() as u16
214 } else {
215 1
216 };
217 if target_cols == 0 {
218 target_cols = 1;
219 }
220 let mut target_rows = if line_height > 0.0 {
221 (height / line_height).floor() as u16
222 } else {
223 1
224 };
225 if target_rows == 0 {
226 target_rows = 1;
227 }
228
229 self.handle.resize(target_rows, target_cols);
230
231 if let Some(on_measured) = &self.on_measured {
233 on_measured.call((char_width, line_height));
234 }
235
236 Some((
237 torin::prelude::Size2D::new(context.area_size.width.max(100.0), height),
238 Rc::new(RefCell::new(FifoCache::<u64, Rc<SkParagraph>>::new())),
239 ))
240 }
241
242 fn render(&self, context: freya_core::element::RenderContext) {
243 let area = context.layout_node.visible_area();
244 let cache = context
245 .layout_node
246 .data
247 .as_ref()
248 .unwrap()
249 .downcast_ref::<RefCell<FifoCache<u64, Rc<SkParagraph>>>>()
250 .unwrap();
251
252 let buffer = self.handle.read_buffer();
253
254 let mut paint = Paint::default();
255 paint.set_style(PaintStyle::Fill);
256 paint.set_color(self.background);
257 context.canvas.draw_rect(
258 SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
259 &paint,
260 );
261
262 let mut text_style = TextStyle::new();
263 text_style.set_color(self.foreground);
264 text_style.set_font_families(&[self.font_family.as_str()]);
265 text_style.set_font_size(self.font_size);
266
267 let mut measure_builder =
268 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
269 measure_builder.push_style(&text_style);
270 measure_builder.add_text("W");
271 let mut measure_paragraph = measure_builder.build();
272 measure_paragraph.layout(f32::MAX);
273 let char_width = measure_paragraph.max_intrinsic_width();
274 let mut line_height = measure_paragraph.height();
275 if line_height <= 0.0 || line_height.is_nan() {
276 line_height = (self.font_size * 1.2).max(1.0);
277 }
278
279 let mut y = area.min_y();
280
281 for (row_idx, row) in buffer.rows.iter().enumerate() {
282 if y + line_height > area.max_y() {
283 break;
284 }
285
286 if let Some(selection) = &buffer.selection {
287 let (display_start, start_col, display_end, end_col) =
288 selection.display_positions(buffer.scroll_offset);
289 let row_i = row_idx as i64;
290
291 if !selection.is_empty() && row_i >= display_start && row_i <= display_end {
292 let sel_start_col = if row_i == display_start { start_col } else { 0 };
293 let sel_end_col = if row_i == display_end {
294 end_col
295 } else {
296 row.len()
297 };
298
299 for col_idx in sel_start_col..sel_end_col.min(row.len()) {
300 let left = area.min_x() + (col_idx as f32) * char_width;
301 let top = y;
302 let right = left + char_width;
303 let bottom = top + line_height;
304
305 let mut sel_paint = Paint::default();
306 sel_paint.set_style(PaintStyle::Fill);
307 sel_paint.set_color(self.selection_color);
308 context
309 .canvas
310 .draw_rect(SkRect::new(left, top, right, bottom), &sel_paint);
311 }
312 }
313 }
314
315 for (col_idx, cell) in row.iter().enumerate() {
316 if cell.is_wide_continuation() {
317 continue;
318 }
319 let cell_bg = map_vt100_color(cell.bgcolor(), self.background);
320 if cell_bg != self.background {
321 let left = area.min_x() + (col_idx as f32) * char_width;
322 let top = y;
323 let cell_width = if cell.is_wide() {
324 char_width * 2.0
325 } else {
326 char_width
327 };
328 let right = left + cell_width;
329 let bottom = top + line_height;
330
331 let mut bg_paint = Paint::default();
332 bg_paint.set_style(PaintStyle::Fill);
333 bg_paint.set_color(cell_bg);
334 context
335 .canvas
336 .draw_rect(SkRect::new(left, top, right, bottom), &bg_paint);
337 }
338 }
339
340 let mut state = FxHasher::default();
341 for cell in row.iter() {
342 if cell.is_wide_continuation() {
343 continue;
344 }
345 let color = map_vt100_color(cell.fgcolor(), self.foreground);
346 cell.contents().hash(&mut state);
347 color.hash(&mut state);
348 }
349
350 let key = state.finish();
351 if let Some(paragraph) = cache.borrow().get(&key) {
352 paragraph.paint(context.canvas, (area.min_x(), y));
353 } else {
354 let mut builder = ParagraphBuilder::new(
355 &ParagraphStyle::default(),
356 context.font_collection.clone(),
357 );
358 for cell in row.iter() {
359 if cell.is_wide_continuation() {
360 continue;
361 }
362 let text = if cell.has_contents() {
363 cell.contents()
364 } else {
365 " "
366 };
367 let mut cell_style = text_style.clone();
368 cell_style.set_color(map_vt100_color(cell.fgcolor(), self.foreground));
369 builder.push_style(&cell_style);
370 builder.add_text(text);
371 }
372 let mut paragraph = builder.build();
373 paragraph.layout(f32::MAX);
374 paragraph.paint(context.canvas, (area.min_x(), y));
375 cache.borrow_mut().insert(key, Rc::new(paragraph));
376 }
377
378 if row_idx == buffer.cursor_row && buffer.scroll_offset == 0 {
379 let cursor_idx = buffer.cursor_col;
380 let left = area.min_x() + (cursor_idx as f32) * char_width;
381 let top = y;
382 let right = left + char_width.max(1.0);
383 let bottom = top + line_height.max(1.0);
384
385 let mut cursor_paint = Paint::default();
386 cursor_paint.set_style(PaintStyle::Fill);
387 cursor_paint.set_color(self.foreground);
388 context
389 .canvas
390 .draw_rect(SkRect::new(left, top, right, bottom), &cursor_paint);
391
392 let content = row
393 .get(cursor_idx)
394 .map(|cell| {
395 if cell.has_contents() {
396 cell.contents()
397 } else {
398 " "
399 }
400 })
401 .unwrap_or(" ");
402
403 let mut fg_text_style = text_style.clone();
404 fg_text_style.set_color(self.background);
405 let mut fg_builder = ParagraphBuilder::new(
406 &ParagraphStyle::default(),
407 context.font_collection.clone(),
408 );
409 fg_builder.push_style(&fg_text_style);
410 fg_builder.add_text(content);
411 let mut fg_paragraph = fg_builder.build();
412 fg_paragraph.layout((right - left).max(1.0));
413 fg_paragraph.paint(context.canvas, (left, top));
414 }
415
416 y += line_height;
417 }
418
419 if buffer.total_scrollback > 0 {
421 let viewport_height = area.height();
422 let total_rows = buffer.rows_count + buffer.total_scrollback;
423 let total_content_height = total_rows as f32 * line_height;
424
425 let scrollbar_height =
426 (viewport_height * viewport_height / total_content_height).max(20.0);
427 let track_height = viewport_height - scrollbar_height;
428
429 let scroll_ratio = if buffer.total_scrollback > 0 {
430 buffer.scroll_offset as f32 / buffer.total_scrollback as f32
431 } else {
432 0.0
433 };
434
435 let thumb_y_offset = track_height * (1.0 - scroll_ratio);
436
437 let scrollbar_width = 4.0;
438 let scrollbar_x = area.max_x() - scrollbar_width;
439 let scrollbar_y = area.min_y() + thumb_y_offset;
440
441 let corner_radius = 2.0;
442 let mut track_paint = Paint::default();
443 track_paint.set_anti_alias(true);
444 track_paint.set_style(PaintStyle::Fill);
445 track_paint.set_color(Color::from_argb(50, 0, 0, 0));
446 context.canvas.draw_round_rect(
447 SkRect::new(scrollbar_x, area.min_y(), area.max_x(), area.max_y()),
448 corner_radius,
449 corner_radius,
450 &track_paint,
451 );
452
453 let mut thumb_paint = Paint::default();
454 thumb_paint.set_anti_alias(true);
455 thumb_paint.set_style(PaintStyle::Fill);
456 thumb_paint.set_color(Color::from_argb(60, 255, 255, 255));
457 context.canvas.draw_round_rect(
458 SkRect::new(
459 scrollbar_x,
460 scrollbar_y,
461 area.max_x(),
462 scrollbar_y + scrollbar_height,
463 ),
464 corner_radius,
465 corner_radius,
466 &thumb_paint,
467 );
468 }
469 }
470}
471
472impl From<Terminal> for Element {
473 fn from(value: Terminal) -> Self {
474 Element::Element {
475 key: DiffKey::None,
476 element: Rc::new(value),
477 elements: Vec::new(),
478 }
479 }
480}