terminal_element.rs

  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}