terminal_element.rs

  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}