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