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 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}