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 fg: Color,
52 bg: 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.fg == other.fg
64 && self.bg == other.bg
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 fg: (220, 220, 220).into(),
77 bg: (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.fg = foreground.into();
108 self
109 }
110
111 pub fn background(mut self, background: impl Into<Color>) -> Self {
112 self.bg = 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(el) = (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 != el.font_size
138 || self.font_family != el.font_family
139 || self.handle != el.handle
140 || self.event_handlers.len() != el.event_handlers.len()
141 {
142 diff.insert(DiffModifies::STYLE);
143 diff.insert(DiffModifies::LAYOUT);
144 }
145
146 diff
147 }
148
149 fn layout(&'_ self) -> Cow<'_, LayoutData> {
150 Cow::Borrowed(&self.layout_data)
151 }
152
153 fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
154 Some(Cow::Borrowed(&self.event_handlers))
155 }
156
157 fn should_hook_measurement(&self) -> bool {
158 true
159 }
160
161 fn measure(
162 &self,
163 context: freya_core::element::LayoutContext,
164 ) -> Option<(torin::prelude::Size2D, Rc<dyn Any>)> {
165 let mut measure_builder =
166 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
167 let mut text_style = TextStyle::new();
168 text_style.set_font_size(self.font_size);
169 text_style.set_font_families(&[self.font_family.as_str()]);
170 measure_builder.push_style(&text_style);
171 measure_builder.add_text("W");
172 let mut measure_paragraph = measure_builder.build();
173 measure_paragraph.layout(f32::MAX);
174 let mut line_height = measure_paragraph.height();
175 if line_height <= 0.0 || line_height.is_nan() {
176 line_height = (self.font_size * 1.2).max(1.0);
177 }
178
179 let mut height = context.area_size.height;
180 if height <= 0.0 {
181 height = (line_height * 24.0).max(200.0);
182 }
183
184 let char_width = measure_paragraph.max_intrinsic_width();
185 let mut target_cols = if char_width > 0.0 {
186 (context.area_size.width / char_width).floor() as u16
187 } else {
188 1
189 };
190 if target_cols == 0 {
191 target_cols = 1;
192 }
193 let mut target_rows = if line_height > 0.0 {
194 (height / line_height).floor() as u16
195 } else {
196 1
197 };
198 if target_rows == 0 {
199 target_rows = 1;
200 }
201
202 self.handle.resize(target_rows, target_cols);
203
204 if let Some(on_measured) = &self.on_measured {
206 on_measured.call((char_width, line_height));
207 }
208
209 Some((
210 torin::prelude::Size2D::new(context.area_size.width.max(100.0), height),
211 Rc::new(RefCell::new(FifoCache::<u64, Rc<SkParagraph>>::new())),
212 ))
213 }
214
215 fn render(&self, context: freya_core::element::RenderContext) {
216 let area = context.layout_node.visible_area();
217 let cache = context
218 .layout_node
219 .data
220 .as_ref()
221 .unwrap()
222 .downcast_ref::<RefCell<FifoCache<u64, Rc<SkParagraph>>>>()
223 .unwrap();
224
225 let buffer = self.handle.read_buffer();
226
227 let mut paint = Paint::default();
228 paint.set_style(PaintStyle::Fill);
229 paint.set_color(self.bg);
230 context.canvas.draw_rect(
231 SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
232 &paint,
233 );
234
235 let mut text_style = TextStyle::new();
236 text_style.set_color(self.fg);
237 text_style.set_font_families(&[self.font_family.as_str()]);
238 text_style.set_font_size(self.font_size);
239
240 let mut measure_builder =
241 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
242 measure_builder.push_style(&text_style);
243 measure_builder.add_text("W");
244 let mut measure_paragraph = measure_builder.build();
245 measure_paragraph.layout(f32::MAX);
246 let char_width = measure_paragraph.max_intrinsic_width();
247 let mut line_height = measure_paragraph.height();
248 if line_height <= 0.0 || line_height.is_nan() {
249 line_height = (self.font_size * 1.2).max(1.0);
250 }
251
252 let mut y = area.min_y();
253
254 for (row_idx, row) in buffer.rows.iter().enumerate() {
255 if y + line_height > area.max_y() {
256 break;
257 }
258
259 if let Some(selection) = &buffer.selection {
260 let (display_start, start_col, display_end, end_col) =
261 selection.display_positions(buffer.scroll_offset);
262 let row_i = row_idx as i64;
263
264 if !selection.is_empty() && row_i >= display_start && row_i <= display_end {
265 let sel_start_col = if row_i == display_start { start_col } else { 0 };
266 let sel_end_col = if row_i == display_end {
267 end_col
268 } else {
269 row.len()
270 };
271
272 for col_idx in sel_start_col..sel_end_col.min(row.len()) {
273 let left = area.min_x() + (col_idx as f32) * char_width;
274 let top = y;
275 let right = left + char_width;
276 let bottom = top + line_height;
277
278 let mut sel_paint = Paint::default();
279 sel_paint.set_style(PaintStyle::Fill);
280 sel_paint.set_color(self.selection_color);
281 context
282 .canvas
283 .draw_rect(SkRect::new(left, top, right, bottom), &sel_paint);
284 }
285 }
286 }
287
288 for (col_idx, cell) in row.iter().enumerate() {
289 if cell.is_wide_continuation() {
290 continue;
291 }
292 let cell_bg = map_vt100_color(cell.bgcolor(), self.bg);
293 if cell_bg != self.bg {
294 let left = area.min_x() + (col_idx as f32) * char_width;
295 let top = y;
296 let cell_width = if cell.is_wide() {
297 char_width * 2.0
298 } else {
299 char_width
300 };
301 let right = left + cell_width;
302 let bottom = top + line_height;
303
304 let mut bg_paint = Paint::default();
305 bg_paint.set_style(PaintStyle::Fill);
306 bg_paint.set_color(cell_bg);
307 context
308 .canvas
309 .draw_rect(SkRect::new(left, top, right, bottom), &bg_paint);
310 }
311 }
312
313 let mut state = FxHasher::default();
314 for cell in row.iter() {
315 if cell.is_wide_continuation() {
316 continue;
317 }
318 let color = map_vt100_color(cell.fgcolor(), self.fg);
319 cell.contents().hash(&mut state);
320 color.hash(&mut state);
321 }
322
323 let key = state.finish();
324 if let Some(paragraph) = cache.borrow().get(&key) {
325 paragraph.paint(context.canvas, (area.min_x(), y));
326 } else {
327 let mut builder = ParagraphBuilder::new(
328 &ParagraphStyle::default(),
329 context.font_collection.clone(),
330 );
331 for cell in row.iter() {
332 if cell.is_wide_continuation() {
333 continue;
334 }
335 let text = if cell.has_contents() {
336 cell.contents()
337 } else {
338 " "
339 };
340 let mut cell_style = text_style.clone();
341 cell_style.set_color(map_vt100_color(cell.fgcolor(), self.fg));
342 builder.push_style(&cell_style);
343 builder.add_text(text);
344 }
345 let mut paragraph = builder.build();
346 paragraph.layout(f32::MAX);
347 paragraph.paint(context.canvas, (area.min_x(), y));
348 cache.borrow_mut().insert(key, Rc::new(paragraph));
349 }
350
351 if row_idx == buffer.cursor_row && buffer.scroll_offset == 0 {
352 let cursor_idx = buffer.cursor_col;
353 let left = area.min_x() + (cursor_idx as f32) * char_width;
354 let top = y;
355 let right = left + char_width.max(1.0);
356 let bottom = top + line_height.max(1.0);
357
358 let mut cursor_paint = Paint::default();
359 cursor_paint.set_style(PaintStyle::Fill);
360 cursor_paint.set_color(self.fg);
361 context
362 .canvas
363 .draw_rect(SkRect::new(left, top, right, bottom), &cursor_paint);
364
365 let content = row
366 .get(cursor_idx)
367 .map(|cell| {
368 if cell.has_contents() {
369 cell.contents()
370 } else {
371 " "
372 }
373 })
374 .unwrap_or(" ");
375
376 let mut fg_text_style = text_style.clone();
377 fg_text_style.set_color(self.bg);
378 let mut fg_builder = ParagraphBuilder::new(
379 &ParagraphStyle::default(),
380 context.font_collection.clone(),
381 );
382 fg_builder.push_style(&fg_text_style);
383 fg_builder.add_text(content);
384 let mut fg_paragraph = fg_builder.build();
385 fg_paragraph.layout((right - left).max(1.0));
386 fg_paragraph.paint(context.canvas, (left, top));
387 }
388
389 y += line_height;
390 }
391
392 if buffer.total_scrollback > 0 {
394 let viewport_height = area.height();
395 let total_rows = buffer.rows_count + buffer.total_scrollback;
396 let total_content_height = total_rows as f32 * line_height;
397
398 let scrollbar_height =
399 (viewport_height * viewport_height / total_content_height).max(20.0);
400 let track_height = viewport_height - scrollbar_height;
401
402 let scroll_ratio = if buffer.total_scrollback > 0 {
403 buffer.scroll_offset as f32 / buffer.total_scrollback as f32
404 } else {
405 0.0
406 };
407
408 let thumb_y_offset = track_height * (1.0 - scroll_ratio);
409
410 let scrollbar_width = 4.0;
411 let scrollbar_x = area.max_x() - scrollbar_width;
412 let scrollbar_y = area.min_y() + thumb_y_offset;
413
414 let corner_radius = 2.0;
415 let mut track_paint = Paint::default();
416 track_paint.set_anti_alias(true);
417 track_paint.set_style(PaintStyle::Fill);
418 track_paint.set_color(Color::from_argb(50, 0, 0, 0));
419 context.canvas.draw_round_rect(
420 SkRect::new(scrollbar_x, area.min_y(), area.max_x(), area.max_y()),
421 corner_radius,
422 corner_radius,
423 &track_paint,
424 );
425
426 let mut thumb_paint = Paint::default();
427 thumb_paint.set_anti_alias(true);
428 thumb_paint.set_style(PaintStyle::Fill);
429 thumb_paint.set_color(Color::from_argb(60, 255, 255, 255));
430 context.canvas.draw_round_rect(
431 SkRect::new(
432 scrollbar_x,
433 scrollbar_y,
434 area.max_x(),
435 scrollbar_y + scrollbar_height,
436 ),
437 corner_radius,
438 corner_radius,
439 &thumb_paint,
440 );
441 }
442 }
443}
444
445impl From<Terminal> for Element {
446 fn from(value: Terminal) -> Self {
447 Element::Element {
448 key: DiffKey::None,
449 element: Rc::new(value),
450 elements: Vec::new(),
451 }
452 }
453}