freya_terminal/
element.rs1use std::{
2 any::Any,
3 borrow::Cow,
4 rc::Rc,
5};
6
7use freya_core::{
8 data::LayoutData,
9 diff_key::DiffKey,
10 element::{
11 Element,
12 ElementExt,
13 },
14 prelude::*,
15 tree::DiffModifies,
16};
17use freya_engine::prelude::{
18 Paint,
19 PaintStyle,
20 ParagraphBuilder,
21 ParagraphStyle,
22 SkRect,
23 TextStyle,
24};
25
26use crate::{
27 colors::{
28 map_vt100_bg_color,
29 map_vt100_fg_color,
30 },
31 handle::TerminalHandle,
32};
33
34#[derive(Clone)]
36pub struct TerminalElement {
37 handle: TerminalHandle,
38 layout_data: LayoutData,
39 font_family: String,
40 font_size: f32,
41 fg: Color,
42 bg: Color,
43}
44
45impl PartialEq for TerminalElement {
46 fn eq(&self, other: &Self) -> bool {
47 self.handle == other.handle
48 && self.font_size == other.font_size
49 && self.font_family == other.font_family
50 && self.fg == other.fg
51 && self.bg == other.bg
52 }
53}
54
55impl TerminalElement {
56 pub(crate) fn new(handle: TerminalHandle) -> Self {
57 Self {
58 handle,
59 layout_data: Default::default(),
60 font_family: "Cascadia Code".to_string(),
61 font_size: 14.,
62 fg: (220, 220, 220).into(),
63 bg: (10, 10, 10).into(),
64 }
65 }
66
67 pub fn font_family(mut self, font_family: impl Into<String>) -> Self {
68 self.font_family = font_family.into();
69 self
70 }
71
72 pub fn font_size(mut self, font_size: f32) -> Self {
73 self.font_size = font_size;
74 self
75 }
76
77 pub fn foreground(mut self, foreground: impl Into<Color>) -> Self {
78 self.fg = foreground.into();
79 self
80 }
81
82 pub fn background(mut self, background: impl Into<Color>) -> Self {
83 self.bg = background.into();
84 self
85 }
86}
87
88impl ElementExt for TerminalElement {
89 fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
90 let Some(el) = (other.as_ref() as &dyn Any).downcast_ref::<TerminalElement>() else {
91 return DiffModifies::all();
92 };
93
94 let mut diff = DiffModifies::empty();
95
96 if self.font_size != el.font_size
97 || self.font_family != el.font_family
98 || self.handle != el.handle
99 {
100 diff.insert(DiffModifies::STYLE);
101 diff.insert(DiffModifies::LAYOUT);
102 }
103
104 diff
105 }
106
107 fn layout(&'_ self) -> Cow<'_, LayoutData> {
108 Cow::Borrowed(&self.layout_data)
109 }
110
111 fn should_hook_measurement(&self) -> bool {
112 true
113 }
114
115 fn measure(
116 &self,
117 context: freya_core::element::LayoutContext,
118 ) -> Option<(torin::prelude::Size2D, Rc<dyn Any>)> {
119 let mut measure_builder =
120 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
121 let mut text_style = TextStyle::new();
122 text_style.set_font_size(self.font_size);
123 text_style.set_font_families(&[self.font_family.as_str()]);
124 measure_builder.push_style(&text_style);
125 measure_builder.add_text("W");
126 let mut measure_paragraph = measure_builder.build();
127 measure_paragraph.layout(f32::MAX);
128 let mut line_height = measure_paragraph.height();
129 if line_height <= 0.0 || line_height.is_nan() {
130 line_height = (self.font_size * 1.2).max(1.0);
131 }
132
133 let mut height = context.area_size.height;
134 if height <= 0.0 {
135 height = (line_height * 24.0).max(200.0);
136 }
137
138 let char_width = measure_paragraph.max_intrinsic_width();
139 let mut target_cols = if char_width > 0.0 {
140 (context.area_size.width / char_width).floor() as u16
141 } else {
142 1
143 };
144 if target_cols == 0 {
145 target_cols = 1;
146 }
147 let mut target_rows = if line_height > 0.0 {
148 (height / line_height).floor() as u16
149 } else {
150 1
151 };
152 if target_rows == 0 {
153 target_rows = 1;
154 }
155
156 self.handle.resize(target_rows, target_cols);
157
158 Some((
159 torin::prelude::Size2D::new(context.area_size.width.max(100.0), height),
160 Rc::new(()),
161 ))
162 }
163
164 fn render(&self, context: freya_core::element::RenderContext) {
165 let area = context.layout_node.visible_area();
166
167 let buffer = self.handle.read_buffer();
168
169 let mut paint = Paint::default();
170 paint.set_style(PaintStyle::Fill);
171 paint.set_color(self.bg);
172 context.canvas.draw_rect(
173 SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
174 &paint,
175 );
176
177 let mut text_style = TextStyle::new();
178 text_style.set_color(self.fg);
179 text_style.set_font_families(&[self.font_family.as_str()]);
180 text_style.set_font_size(self.font_size);
181
182 let mut measure_builder =
183 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
184 measure_builder.push_style(&text_style);
185 measure_builder.add_text("W");
186 let mut measure_paragraph = measure_builder.build();
187 measure_paragraph.layout(f32::MAX);
188 let char_width = measure_paragraph.max_intrinsic_width();
189 let mut line_height = measure_paragraph.height();
190 if line_height <= 0.0 || line_height.is_nan() {
191 line_height = (self.font_size * 1.2).max(1.0);
192 }
193
194 let mut y = area.min_y();
195
196 for (row_idx, row) in buffer.rows.iter().enumerate() {
197 if y + line_height > area.max_y() {
198 break;
199 }
200
201 for (col_idx, cell) in row.iter().enumerate() {
202 if cell.is_wide_continuation() {
203 continue;
204 }
205 let cell_bg = map_vt100_bg_color(cell.bgcolor(), self.fg, self.bg);
206 if cell_bg != self.bg {
207 let left = area.min_x() + (col_idx as f32) * char_width;
208 let top = y;
209 let cell_width = if cell.is_wide() {
210 char_width * 2.0
211 } else {
212 char_width
213 };
214 let right = left + cell_width;
215 let bottom = top + line_height;
216
217 let mut bg_paint = Paint::default();
218 bg_paint.set_style(PaintStyle::Fill);
219 bg_paint.set_color(cell_bg);
220 context
221 .canvas
222 .draw_rect(SkRect::new(left, top, right, bottom), &bg_paint);
223 }
224 }
225
226 let mut builder =
227 ParagraphBuilder::new(&ParagraphStyle::default(), context.font_collection.clone());
228 for cell in row.iter() {
229 if cell.is_wide_continuation() {
230 continue;
231 }
232 let text = if cell.has_contents() {
233 cell.contents()
234 } else {
235 " "
236 };
237 let mut cell_style = text_style.clone();
238 cell_style.set_color(map_vt100_fg_color(cell.fgcolor(), self.fg, self.bg));
239 builder.push_style(&cell_style);
240 builder.add_text(text);
241 }
242 let mut paragraph = builder.build();
243 paragraph.layout(f32::MAX);
244 paragraph.paint(context.canvas, (area.min_x(), y));
245
246 if row_idx == buffer.cursor_row {
247 let cursor_idx = buffer.cursor_col;
248 let left = area.min_x() + (cursor_idx as f32) * char_width;
249 let top = y;
250 let right = left + char_width.max(1.0);
251 let bottom = top + line_height.max(1.0);
252
253 let mut cursor_paint = Paint::default();
254 cursor_paint.set_style(PaintStyle::Fill);
255 cursor_paint.set_color(self.fg);
256 context
257 .canvas
258 .draw_rect(SkRect::new(left, top, right, bottom), &cursor_paint);
259
260 let content = row
261 .get(cursor_idx)
262 .map(|cell| {
263 if cell.has_contents() {
264 cell.contents()
265 } else {
266 " "
267 }
268 })
269 .unwrap_or(" ");
270
271 let mut fg_text_style = text_style.clone();
272 fg_text_style.set_color(self.bg);
273 let mut fg_builder = ParagraphBuilder::new(
274 &ParagraphStyle::default(),
275 context.font_collection.clone(),
276 );
277 fg_builder.push_style(&fg_text_style);
278 fg_builder.add_text(content);
279 let mut fg_paragraph = fg_builder.build();
280 fg_paragraph.layout((right - left).max(1.0));
281 fg_paragraph.paint(context.canvas, (left, top));
282 }
283
284 y += line_height;
285 }
286 }
287}
288
289impl From<TerminalElement> for Element {
290 fn from(value: TerminalElement) -> Self {
291 Element::Element {
292 key: DiffKey::None,
293 element: Rc::new(value),
294 elements: Vec::new(),
295 }
296 }
297}