1use alacritty_terminal::{
2 ansi::Color as AnsiColor,
3 grid::{GridIterator, Indexed},
4 index::Point,
5 term::{
6 cell::{Cell, Flags},
7 SizeInfo,
8 },
9};
10use gpui::{
11 color::Color,
12 elements::*,
13 fonts::{HighlightStyle, TextStyle, Underline},
14 geometry::{rect::RectF, vector::vec2f},
15 json::json,
16 text_layout::Line,
17 Event, MouseRegion, PaintContext, Quad, WeakViewHandle,
18};
19use ordered_float::OrderedFloat;
20use settings::Settings;
21use std::rc::Rc;
22use theme::TerminalStyle;
23
24use crate::{Input, ScrollTerminal, Terminal};
25
26const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
27
28#[cfg(debug_assertions)]
29const DEBUG_GRID: bool = false;
30
31pub struct TerminalEl {
32 view: WeakViewHandle<Terminal>,
33}
34
35impl TerminalEl {
36 pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
37 TerminalEl { view }
38 }
39}
40
41pub struct LayoutState {
42 lines: Vec<Line>,
43 line_height: f32,
44 em_width: f32,
45 cursor: Option<(RectF, Color)>,
46 cur_size: SizeInfo,
47 background_color: Color,
48}
49
50impl Element for TerminalEl {
51 type LayoutState = LayoutState;
52 type PaintState = ();
53
54 fn layout(
55 &mut self,
56 constraint: gpui::SizeConstraint,
57 cx: &mut gpui::LayoutContext,
58 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
59 let view = self.view.upgrade(cx).unwrap();
60 let size = constraint.max;
61 let settings = cx.global::<Settings>();
62 let editor_theme = &settings.theme.editor;
63 let font_cache = cx.font_cache();
64
65 //Set up text rendering
66 let text_style = TextStyle {
67 color: editor_theme.text_color,
68 font_family_id: settings.buffer_font_family,
69 font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
70 font_id: font_cache
71 .select_font(settings.buffer_font_family, &Default::default())
72 .unwrap(),
73 font_size: settings.buffer_font_size,
74 font_properties: Default::default(),
75 underline: Default::default(),
76 };
77
78 let line_height = font_cache.line_height(text_style.font_size);
79 let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
80
81 let new_size = SizeInfo::new(
82 size.x() - cell_width,
83 size.y(),
84 cell_width,
85 line_height,
86 0.,
87 0.,
88 false,
89 );
90 view.update(cx.app, |view, _cx| {
91 view.set_size(new_size);
92 });
93
94 let settings = cx.global::<Settings>();
95 let terminal_theme = &settings.theme.terminal;
96 let term = view.read(cx).term.lock();
97
98 let content = term.renderable_content();
99 let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme);
100
101 let shaped_lines = layout_highlighted_chunks(
102 chunks.iter().map(|(text, style)| (text.as_str(), *style)),
103 &text_style,
104 cx.text_layout_cache,
105 &cx.font_cache,
106 usize::MAX,
107 line_count,
108 );
109
110 let cursor_line = content.cursor.point.line.0 + content.display_offset as i32;
111 let mut cursor = None;
112 if let Some(layout_line) = cursor_line
113 .try_into()
114 .ok()
115 .and_then(|cursor_line: usize| shaped_lines.get(cursor_line))
116 {
117 let cursor_x = layout_line.x_for_index(content.cursor.point.column.0);
118 cursor = Some((
119 RectF::new(
120 vec2f(cursor_x, cursor_line as f32 * line_height),
121 vec2f(cell_width, line_height),
122 ),
123 terminal_theme.cursor,
124 ));
125 }
126
127 (
128 constraint.max,
129 LayoutState {
130 lines: shaped_lines,
131 line_height,
132 em_width: cell_width,
133 cursor,
134 cur_size: new_size,
135 background_color: terminal_theme.background,
136 },
137 )
138 }
139
140 fn paint(
141 &mut self,
142 bounds: gpui::geometry::rect::RectF,
143 visible_bounds: gpui::geometry::rect::RectF,
144 layout: &mut Self::LayoutState,
145 cx: &mut gpui::PaintContext,
146 ) -> Self::PaintState {
147 cx.scene.push_layer(Some(visible_bounds));
148
149 cx.scene.push_mouse_region(MouseRegion {
150 view_id: self.view.id(),
151 discriminant: None,
152 bounds: visible_bounds,
153 hover: None,
154 mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
155 click: None,
156 right_mouse_down: None,
157 right_click: None,
158 drag: None,
159 mouse_down_out: None,
160 right_mouse_down_out: None,
161 });
162
163 //Background
164 cx.scene.push_quad(Quad {
165 bounds: visible_bounds,
166 background: Some(layout.background_color),
167 border: Default::default(),
168 corner_radius: 0.,
169 });
170
171 let origin = bounds.origin() + vec2f(layout.em_width, 0.); //Padding
172
173 let mut line_origin = origin;
174 for line in &layout.lines {
175 let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height));
176
177 if boundaries.intersects(visible_bounds) {
178 line.paint(line_origin, visible_bounds, layout.line_height, cx);
179 }
180
181 line_origin.set_y(boundaries.max_y());
182 }
183
184 if let Some((c, color)) = layout.cursor {
185 let new_origin = origin + c.origin();
186 let new_cursor = RectF::new(new_origin, c.size());
187 cx.scene.push_quad(Quad {
188 bounds: new_cursor,
189 background: Some(color),
190 border: Default::default(),
191 corner_radius: 0.,
192 });
193 }
194
195 #[cfg(debug_assertions)]
196 if DEBUG_GRID {
197 draw_debug_grid(bounds, layout, cx);
198 }
199
200 cx.scene.pop_layer();
201 }
202
203 fn dispatch_event(
204 &mut self,
205 event: &gpui::Event,
206 _bounds: gpui::geometry::rect::RectF,
207 visible_bounds: gpui::geometry::rect::RectF,
208 layout: &mut Self::LayoutState,
209 _paint: &mut Self::PaintState,
210 cx: &mut gpui::EventContext,
211 ) -> bool {
212 match event {
213 Event::ScrollWheel {
214 delta, position, ..
215 } => {
216 if visible_bounds.contains_point(*position) {
217 let vertical_scroll =
218 (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
219 cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
220 true
221 } else {
222 false
223 }
224 }
225 Event::KeyDown {
226 input: Some(input), ..
227 } => {
228 if cx.is_parent_view_focused() {
229 cx.dispatch_action(Input(input.to_string()));
230 true
231 } else {
232 false
233 }
234 }
235 _ => false,
236 }
237 }
238
239 fn debug(
240 &self,
241 _bounds: gpui::geometry::rect::RectF,
242 _layout: &Self::LayoutState,
243 _paint: &Self::PaintState,
244 _cx: &gpui::DebugContext,
245 ) -> gpui::serde_json::Value {
246 json!({
247 "type": "TerminalElement",
248 })
249 }
250}
251
252pub(crate) fn build_chunks(
253 grid_iterator: GridIterator<Cell>,
254 theme: &TerminalStyle,
255) -> (Vec<(String, Option<HighlightStyle>)>, usize) {
256 let mut lines: Vec<(String, Option<HighlightStyle>)> = vec![];
257 let mut last_line = 0;
258 let mut line_count = 1;
259 let mut cur_chunk = String::new();
260
261 let mut cur_highlight = HighlightStyle {
262 color: Some(Color::white()),
263 ..Default::default()
264 };
265
266 for cell in grid_iterator {
267 let Indexed {
268 point: Point { line, .. },
269 cell: Cell {
270 c, fg, flags, .. // TODO: Add bg and flags
271 }, //TODO: Learn what 'CellExtra does'
272 } = cell;
273
274 let new_highlight = make_style_from_cell(fg, flags, theme);
275
276 if line != last_line {
277 line_count += 1;
278 cur_chunk.push('\n');
279 last_line = line.0;
280 }
281
282 if new_highlight != cur_highlight {
283 lines.push((cur_chunk.clone(), Some(cur_highlight.clone())));
284 cur_chunk.clear();
285 cur_highlight = new_highlight;
286 }
287 cur_chunk.push(*c)
288 }
289 lines.push((cur_chunk, Some(cur_highlight)));
290 (lines, line_count)
291}
292
293fn make_style_from_cell(fg: &AnsiColor, flags: &Flags, style: &TerminalStyle) -> HighlightStyle {
294 let fg = Some(alac_color_to_gpui_color(fg, style));
295 let underline = if flags.contains(Flags::UNDERLINE) {
296 Some(Underline {
297 color: fg,
298 squiggly: false,
299 thickness: OrderedFloat(1.),
300 })
301 } else {
302 None
303 };
304 HighlightStyle {
305 color: fg,
306 underline,
307 ..Default::default()
308 }
309}
310
311fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> Color {
312 match allac_color {
313 alacritty_terminal::ansi::Color::Named(n) => match n {
314 alacritty_terminal::ansi::NamedColor::Black => style.black,
315 alacritty_terminal::ansi::NamedColor::Red => style.red,
316 alacritty_terminal::ansi::NamedColor::Green => style.green,
317 alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
318 alacritty_terminal::ansi::NamedColor::Blue => style.blue,
319 alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
320 alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
321 alacritty_terminal::ansi::NamedColor::White => style.white,
322 alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
323 alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
324 alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
325 alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
326 alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
327 alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
328 alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
329 alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
330 alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
331 alacritty_terminal::ansi::NamedColor::Background => style.background,
332 alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
333 alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
334 alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
335 alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
336 alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
337 alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
338 alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
339 alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
340 alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
341 alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
342 alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
343 }, //Theme defined
344 alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1),
345 alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), //Color cube weirdness
346 }
347}
348
349pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
350 match index {
351 0 => style.black,
352 1 => style.red,
353 2 => style.green,
354 3 => style.yellow,
355 4 => style.blue,
356 5 => style.magenta,
357 6 => style.cyan,
358 7 => style.white,
359 8 => style.bright_black,
360 9 => style.bright_red,
361 10 => style.bright_green,
362 11 => style.bright_yellow,
363 12 => style.bright_blue,
364 13 => style.bright_magenta,
365 14 => style.bright_cyan,
366 15 => style.bright_white,
367 16..=231 => {
368 let (r, g, b) = rgb_for_index(index); //Split the index into it's rgb components
369 let step = (u8::MAX as f32 / 5.).round() as u8; //Split the GPUI range into 5 chunks
370 Color::new(r * step, g * step, b * step, 1) //Map the rgb components to GPUI's range
371 }
372 //Grayscale from black to white, 0 to 24
373 232..=255 => {
374 let i = 24 - (index - 232); //Align index to 24..0
375 let step = (u8::MAX as f32 / 24.).round() as u8; //Split the 256 range grayscale into 24 chunks
376 Color::new(i * step, i * step, i * step, 1) //Map the rgb components to GPUI's range
377 }
378 }
379}
380
381///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
382///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
383///
384///Wikipedia gives a formula for calculating the index for a given color:
385///
386///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
387///
388///This function does the reverse, calculating the r, g, and b components from a given index.
389fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
390 debug_assert!(i >= &16 && i <= &231);
391 let i = i - 16;
392 let r = (i - (i % 36)) / 36;
393 let g = ((i % 36) - (i % 6)) / 6;
394 let b = (i % 36) % 6;
395 (r, g, b)
396}
397
398#[cfg(debug_assertions)]
399fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
400 let width = layout.cur_size.width();
401 let height = layout.cur_size.height();
402 //Alacritty uses 'as usize', so shall we.
403 for col in 0..(width / layout.em_width).round() as usize {
404 cx.scene.push_quad(Quad {
405 bounds: RectF::new(
406 bounds.origin() + vec2f((col + 1) as f32 * layout.em_width, 0.),
407 vec2f(1., height),
408 ),
409 background: Some(Color::green()),
410 border: Default::default(),
411 corner_radius: 0.,
412 });
413 }
414 for row in 0..((height / layout.line_height) + 1.0).round() as usize {
415 cx.scene.push_quad(Quad {
416 bounds: RectF::new(
417 bounds.origin() + vec2f(layout.em_width, row as f32 * layout.line_height),
418 vec2f(width, 1.),
419 ),
420 background: Some(Color::green()),
421 border: Default::default(),
422 corner_radius: 0.,
423 });
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 #[test]
430 fn test_rgb_for_index() {
431 //Test every possible value in the color cube
432 for i in 16..=231 {
433 let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
434 assert_eq!(i, 16 + 36 * r + 6 * g + b);
435 }
436 }
437}