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