1use alacritty_terminal::{
2 ansi::Color as AnsiColor,
3 grid::{Dimensions, GridIterator, Indexed},
4 index::Point,
5 term::{
6 cell::{Cell, Flags},
7 SizeInfo,
8 },
9};
10use editor::{Cursor, CursorShape};
11use gpui::{
12 color::Color,
13 elements::*,
14 fonts::{TextStyle, Underline},
15 geometry::{
16 rect::RectF,
17 vector::{vec2f, Vector2F},
18 },
19 json::json,
20 text_layout::{Line, RunStyle},
21 Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache,
22 WeakViewHandle,
23};
24use itertools::Itertools;
25use ordered_float::OrderedFloat;
26use settings::Settings;
27use std::rc::Rc;
28use theme::TerminalStyle;
29
30use crate::{gpui_func_tools::paint_layer, Input, ScrollTerminal, Terminal};
31
32///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
33///Scroll multiplier that is set to 3 by default. This will be removed when I
34///Implement scroll bars.
35const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
36
37///Used to display the grid as passed to Alacritty and the TTY.
38///Useful for debugging inconsistencies between behavior and display
39#[cfg(debug_assertions)]
40const DEBUG_GRID: bool = false;
41
42///The GPUI element that paints the terminal.
43pub struct TerminalEl {
44 view: WeakViewHandle<Terminal>,
45}
46
47///Helper types so I don't mix these two up
48struct CellWidth(f32);
49struct LineHeight(f32);
50
51#[derive(Clone, Debug, Default)]
52struct LayoutCell {
53 point: Point<i32, i32>,
54 text: Line,
55 background_color: Color,
56}
57
58impl LayoutCell {
59 fn new(point: Point<i32, i32>, text: Line, background_color: Color) -> LayoutCell {
60 LayoutCell {
61 point,
62 text,
63 background_color,
64 }
65 }
66}
67
68///The information generated during layout that is nescessary for painting
69pub struct LayoutState {
70 cells: Vec<(Point<i32, i32>, Line)>,
71 background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan
72 line_height: LineHeight,
73 em_width: CellWidth,
74 cursor: Option<Cursor>,
75 background_color: Color,
76 cur_size: SizeInfo,
77}
78
79impl TerminalEl {
80 pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
81 TerminalEl { view }
82 }
83}
84
85impl Element for TerminalEl {
86 type LayoutState = LayoutState;
87 type PaintState = ();
88
89 fn layout(
90 &mut self,
91 constraint: gpui::SizeConstraint,
92 cx: &mut gpui::LayoutContext,
93 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
94 //Settings immutably borrows cx here for the settings and font cache
95 //and we need to modify the cx to resize the terminal. So instead of
96 //storing Settings or the font_cache(), we toss them ASAP and then reborrow later
97 let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
98 let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
99 let cell_width = CellWidth(
100 cx.font_cache()
101 .em_advance(text_style.font_id, text_style.font_size),
102 );
103 let view_handle = self.view.upgrade(cx).unwrap();
104
105 //Tell the view our new size. Requires a mutable borrow of cx and the view
106 let cur_size = make_new_size(constraint, &cell_width, &line_height);
107 //Note that set_size locks and mutates the terminal.
108 //TODO: Would be nice to lock once for the whole of layout
109 view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
110
111 //Now that we're done with the mutable portion, grab the immutable settings and view again
112 let terminal_theme = &(cx.global::<Settings>()).theme.terminal;
113 let term = view_handle.read(cx).term.lock();
114
115 let grid = term.grid();
116 let cursor_point = grid.cursor.point;
117 let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
118
119 let content = term.renderable_content();
120
121 let layout_cells = layout_cells(
122 content.display_iter,
123 &text_style,
124 terminal_theme,
125 cx.text_layout_cache,
126 );
127
128 let cells = layout_cells
129 .iter()
130 .map(|c| (c.point, c.text.clone()))
131 .collect::<Vec<(Point<i32, i32>, Line)>>();
132 let background_rects = layout_cells
133 .iter()
134 .map(|cell| {
135 (
136 RectF::new(
137 vec2f(
138 cell.point.column as f32 * cell_width.0,
139 cell.point.line as f32 * line_height.0,
140 ),
141 vec2f(cell_width.0, line_height.0),
142 ),
143 cell.background_color,
144 )
145 })
146 .collect::<Vec<(RectF, Color)>>();
147
148 let block_text = cx.text_layout_cache.layout_str(
149 &cursor_text,
150 text_style.font_size,
151 &[(
152 cursor_text.len(),
153 RunStyle {
154 font_id: text_style.font_id,
155 color: terminal_theme.background,
156 underline: Default::default(),
157 },
158 )],
159 );
160
161 let cursor = get_cursor_shape(
162 content.cursor.point.line.0 as usize,
163 content.cursor.point.column.0 as usize,
164 content.display_offset,
165 &line_height,
166 &cell_width,
167 cur_size.total_lines(),
168 &block_text,
169 )
170 .map(move |(cursor_position, block_width)| {
171 let block_width = if block_width != 0.0 {
172 block_width
173 } else {
174 cell_width.0
175 };
176
177 Cursor::new(
178 cursor_position,
179 block_width,
180 line_height.0,
181 terminal_theme.cursor,
182 CursorShape::Block,
183 Some(block_text.clone()),
184 )
185 });
186
187 (
188 constraint.max,
189 LayoutState {
190 cells,
191 line_height,
192 em_width: cell_width,
193 cursor,
194 cur_size,
195 background_rects,
196 background_color: terminal_theme.background,
197 },
198 )
199 }
200
201 fn paint(
202 &mut self,
203 bounds: gpui::geometry::rect::RectF,
204 visible_bounds: gpui::geometry::rect::RectF,
205 layout: &mut Self::LayoutState,
206 cx: &mut gpui::PaintContext,
207 ) -> Self::PaintState {
208 //Setup element stuff
209 let clip_bounds = Some(visible_bounds);
210 paint_layer(cx, clip_bounds, |cx| {
211 //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
212 cx.scene.push_mouse_region(MouseRegion {
213 view_id: self.view.id(),
214 mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
215 bounds: visible_bounds,
216 ..Default::default()
217 });
218
219 let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
220
221 paint_layer(cx, clip_bounds, |cx| {
222 //Start with a background color
223 cx.scene.push_quad(Quad {
224 bounds: RectF::new(bounds.origin(), bounds.size()),
225 background: Some(layout.background_color),
226 border: Default::default(),
227 corner_radius: 0.,
228 });
229
230 //Draw cell backgrounds
231 for background_rect in &layout.background_rects {
232 let new_origin = origin + background_rect.0.origin();
233 cx.scene.push_quad(Quad {
234 bounds: RectF::new(new_origin, background_rect.0.size()),
235 background: Some(background_rect.1),
236 border: Default::default(),
237 corner_radius: 0.,
238 })
239 }
240 });
241
242 //Draw text
243 paint_layer(cx, clip_bounds, |cx| {
244 for (point, cell) in &layout.cells {
245 let cell_origin = vec2f(
246 origin.x() + point.column as f32 * layout.em_width.0,
247 origin.y() + point.line as f32 * layout.line_height.0,
248 );
249 cell.paint(cell_origin, visible_bounds, layout.line_height.0, cx);
250 }
251 });
252
253 //Draw cursor
254 if let Some(cursor) = &layout.cursor {
255 paint_layer(cx, clip_bounds, |cx| {
256 cursor.paint(origin, cx);
257 })
258 }
259
260 #[cfg(debug_assertions)]
261 if DEBUG_GRID {
262 paint_layer(cx, clip_bounds, |cx| {
263 draw_debug_grid(bounds, layout, cx);
264 });
265 }
266 });
267 }
268
269 fn dispatch_event(
270 &mut self,
271 event: &gpui::Event,
272 _bounds: gpui::geometry::rect::RectF,
273 visible_bounds: gpui::geometry::rect::RectF,
274 layout: &mut Self::LayoutState,
275 _paint: &mut Self::PaintState,
276 cx: &mut gpui::EventContext,
277 ) -> bool {
278 match event {
279 Event::ScrollWheel {
280 delta, position, ..
281 } => visible_bounds
282 .contains_point(*position)
283 .then(|| {
284 let vertical_scroll =
285 (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
286 cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
287 })
288 .is_some(),
289 Event::KeyDown {
290 input: Some(input), ..
291 } => cx
292 .is_parent_view_focused()
293 .then(|| {
294 cx.dispatch_action(Input(input.to_string()));
295 })
296 .is_some(),
297 _ => false,
298 }
299 }
300
301 fn debug(
302 &self,
303 _bounds: gpui::geometry::rect::RectF,
304 _layout: &Self::LayoutState,
305 _paint: &Self::PaintState,
306 _cx: &gpui::DebugContext,
307 ) -> gpui::serde_json::Value {
308 json!({
309 "type": "TerminalElement",
310 })
311 }
312}
313
314///Configures a text style from the current settings.
315fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
316 TextStyle {
317 color: settings.theme.editor.text_color,
318 font_family_id: settings.buffer_font_family,
319 font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
320 font_id: font_cache
321 .select_font(settings.buffer_font_family, &Default::default())
322 .unwrap(),
323 font_size: settings.buffer_font_size,
324 font_properties: Default::default(),
325 underline: Default::default(),
326 }
327}
328
329///Configures a size info object from the given information.
330fn make_new_size(
331 constraint: SizeConstraint,
332 cell_width: &CellWidth,
333 line_height: &LineHeight,
334) -> SizeInfo {
335 SizeInfo::new(
336 constraint.max.x() - cell_width.0,
337 constraint.max.y(),
338 cell_width.0,
339 line_height.0,
340 0.,
341 0.,
342 false,
343 )
344}
345
346fn layout_cells(
347 grid: GridIterator<Cell>,
348 text_style: &TextStyle,
349 terminal_theme: &TerminalStyle,
350 text_layout_cache: &TextLayoutCache,
351) -> Vec<LayoutCell> {
352 let mut line_count: i32 = 0;
353 let lines = grid.group_by(|i| i.point.line);
354 lines
355 .into_iter()
356 .map(|(_, line)| {
357 line_count += 1;
358 line.map(|indexed_cell| {
359 let cell_text = &indexed_cell.c.to_string();
360
361 let cell_style = cell_style(&indexed_cell, terminal_theme, text_style);
362
363 let layout_cell = text_layout_cache.layout_str(
364 cell_text,
365 text_style.font_size,
366 &[(cell_text.len(), cell_style)],
367 );
368 LayoutCell::new(
369 Point::new(line_count - 1, indexed_cell.point.column.0 as i32),
370 layout_cell,
371 convert_color(&indexed_cell.bg, terminal_theme),
372 )
373 })
374 .collect::<Vec<LayoutCell>>()
375 })
376 .flatten()
377 .collect::<Vec<LayoutCell>>()
378}
379
380// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
381// the same position for sequential indexes. Use em_width instead
382//TODO: This function is messy, too many arguments and too many ifs. Simplify.
383fn get_cursor_shape(
384 line: usize,
385 line_index: usize,
386 display_offset: usize,
387 line_height: &LineHeight,
388 cell_width: &CellWidth,
389 total_lines: usize,
390 text_fragment: &Line,
391) -> Option<(Vector2F, f32)> {
392 let cursor_line = line + display_offset;
393 if cursor_line <= total_lines {
394 let cursor_width = if text_fragment.width() == 0. {
395 cell_width.0
396 } else {
397 text_fragment.width()
398 };
399
400 Some((
401 vec2f(
402 line_index as f32 * cell_width.0,
403 cursor_line as f32 * line_height.0,
404 ),
405 cursor_width,
406 ))
407 } else {
408 None
409 }
410}
411
412///Convert the Alacritty cell styles to GPUI text styles and background color
413fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &TextStyle) -> RunStyle {
414 let flags = indexed.cell.flags;
415 let fg = convert_color(&indexed.cell.fg, style);
416
417 let underline = flags
418 .contains(Flags::UNDERLINE)
419 .then(|| Underline {
420 color: Some(fg),
421 squiggly: false,
422 thickness: OrderedFloat(1.),
423 })
424 .unwrap_or_default();
425
426 RunStyle {
427 color: fg,
428 font_id: text_style.font_id,
429 underline,
430 }
431}
432
433///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
434fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
435 match alac_color {
436 //Named and theme defined colors
437 alacritty_terminal::ansi::Color::Named(n) => match n {
438 alacritty_terminal::ansi::NamedColor::Black => style.black,
439 alacritty_terminal::ansi::NamedColor::Red => style.red,
440 alacritty_terminal::ansi::NamedColor::Green => style.green,
441 alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
442 alacritty_terminal::ansi::NamedColor::Blue => style.blue,
443 alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
444 alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
445 alacritty_terminal::ansi::NamedColor::White => style.white,
446 alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
447 alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
448 alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
449 alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
450 alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
451 alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
452 alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
453 alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
454 alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
455 alacritty_terminal::ansi::NamedColor::Background => style.background,
456 alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
457 alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
458 alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
459 alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
460 alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
461 alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
462 alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
463 alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
464 alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
465 alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
466 alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
467 },
468 //'True' colors
469 alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
470 //8 bit, indexed colors
471 alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style),
472 }
473}
474
475///Converts an 8 bit ANSI color to it's GPUI equivalent.
476pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
477 match index {
478 //0-15 are the same as the named colors above
479 0 => style.black,
480 1 => style.red,
481 2 => style.green,
482 3 => style.yellow,
483 4 => style.blue,
484 5 => style.magenta,
485 6 => style.cyan,
486 7 => style.white,
487 8 => style.bright_black,
488 9 => style.bright_red,
489 10 => style.bright_green,
490 11 => style.bright_yellow,
491 12 => style.bright_blue,
492 13 => style.bright_magenta,
493 14 => style.bright_cyan,
494 15 => style.bright_white,
495 //16-231 are mapped to their RGB colors on a 0-5 range per channel
496 16..=231 => {
497 let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components
498 let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
499 Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
500 }
501 //232-255 are a 24 step grayscale from black to white
502 232..=255 => {
503 let i = index - 232; //Align index to 0..24
504 let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
505 Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
506 }
507 }
508}
509
510///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
511///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
512///
513///Wikipedia gives a formula for calculating the index for a given color:
514///
515///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
516///
517///This function does the reverse, calculating the r, g, and b components from a given index.
518fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
519 debug_assert!(i >= &16 && i <= &231);
520 let i = i - 16;
521 let r = (i - (i % 36)) / 36;
522 let g = ((i % 36) - (i % 6)) / 6;
523 let b = (i % 36) % 6;
524 (r, g, b)
525}
526
527///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
528///Display and conceptual grid.
529#[cfg(debug_assertions)]
530fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
531 let width = layout.cur_size.width();
532 let height = layout.cur_size.height();
533 //Alacritty uses 'as usize', so shall we.
534 for col in 0..(width / layout.em_width.0).round() as usize {
535 cx.scene.push_quad(Quad {
536 bounds: RectF::new(
537 bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
538 vec2f(1., height),
539 ),
540 background: Some(Color::green()),
541 border: Default::default(),
542 corner_radius: 0.,
543 });
544 }
545 for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
546 cx.scene.push_quad(Quad {
547 bounds: RectF::new(
548 bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
549 vec2f(width, 1.),
550 ),
551 background: Some(Color::green()),
552 border: Default::default(),
553 corner_radius: 0.,
554 });
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 #[test]
561 fn test_rgb_for_index() {
562 //Test every possible value in the color cube
563 for i in 16..=231 {
564 let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
565 assert_eq!(i, 16 + 36 * r + 6 * g + b);
566 }
567 }
568}