terminal_element.rs

  1use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
  2use gpui::{
  3    color::Color,
  4    elements::{Empty, Overlay},
  5    fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
  6    geometry::{
  7        rect::RectF,
  8        vector::{vec2f, Vector2F},
  9    },
 10    platform::{CursorStyle, MouseButton},
 11    serde_json::json,
 12    text_layout::{Line, RunStyle},
 13    AnyElement, Element, EventContext, FontCache, ModelContext, MouseRegion, Quad, SizeConstraint,
 14    TextLayoutCache, ViewContext, WeakModelHandle, WindowContext,
 15};
 16use itertools::Itertools;
 17use language::CursorShape;
 18use ordered_float::OrderedFloat;
 19use terminal::{
 20    alacritty_terminal::{
 21        ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
 22        grid::Dimensions,
 23        index::Point,
 24        term::{cell::Flags, TermMode},
 25    },
 26    mappings::colors::convert_color,
 27    terminal_settings::TerminalSettings,
 28    IndexedCell, Terminal, TerminalContent, TerminalSize,
 29};
 30use theme::{TerminalStyle, ThemeSettings};
 31use util::ResultExt;
 32
 33use std::{fmt::Debug, ops::RangeInclusive};
 34use std::{mem, ops::Range};
 35
 36use crate::TerminalView;
 37
 38///The information generated during layout that is necessary for painting
 39pub struct LayoutState {
 40    cells: Vec<LayoutCell>,
 41    rects: Vec<LayoutRect>,
 42    relative_highlighted_ranges: Vec<(RangeInclusive<Point>, Color)>,
 43    cursor: Option<Cursor>,
 44    background_color: Color,
 45    size: TerminalSize,
 46    mode: TermMode,
 47    display_offset: usize,
 48    hyperlink_tooltip: Option<AnyElement<TerminalView>>,
 49    gutter: f32,
 50}
 51
 52///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
 53struct DisplayCursor {
 54    line: i32,
 55    col: usize,
 56}
 57
 58impl DisplayCursor {
 59    fn from(cursor_point: Point, display_offset: usize) -> Self {
 60        Self {
 61            line: cursor_point.line.0 + display_offset as i32,
 62            col: cursor_point.column.0,
 63        }
 64    }
 65
 66    pub fn line(&self) -> i32 {
 67        self.line
 68    }
 69
 70    pub fn col(&self) -> usize {
 71        self.col
 72    }
 73}
 74
 75#[derive(Clone, Debug, Default)]
 76struct LayoutCell {
 77    point: Point<i32, i32>,
 78    text: Line,
 79}
 80
 81impl LayoutCell {
 82    fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
 83        LayoutCell { point, text }
 84    }
 85
 86    fn paint(
 87        &self,
 88        origin: Vector2F,
 89        layout: &LayoutState,
 90        visible_bounds: RectF,
 91        _view: &mut TerminalView,
 92        cx: &mut WindowContext,
 93    ) {
 94        let pos = {
 95            let point = self.point;
 96            vec2f(
 97                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
 98                origin.y() + point.line as f32 * layout.size.line_height,
 99            )
100        };
101
102        self.text
103            .paint(pos, visible_bounds, layout.size.line_height, cx);
104    }
105}
106
107#[derive(Clone, Debug, Default)]
108struct LayoutRect {
109    point: Point<i32, i32>,
110    num_of_cells: usize,
111    color: Color,
112}
113
114impl LayoutRect {
115    fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
116        LayoutRect {
117            point,
118            num_of_cells,
119            color,
120        }
121    }
122
123    fn extend(&self) -> Self {
124        LayoutRect {
125            point: self.point,
126            num_of_cells: self.num_of_cells + 1,
127            color: self.color,
128        }
129    }
130
131    fn paint(
132        &self,
133        origin: Vector2F,
134        layout: &LayoutState,
135        _view: &mut TerminalView,
136        cx: &mut ViewContext<TerminalView>,
137    ) {
138        let position = {
139            let point = self.point;
140            vec2f(
141                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
142                origin.y() + point.line as f32 * layout.size.line_height,
143            )
144        };
145        let size = vec2f(
146            (layout.size.cell_width * self.num_of_cells as f32).ceil(),
147            layout.size.line_height,
148        );
149
150        cx.scene().push_quad(Quad {
151            bounds: RectF::new(position, size),
152            background: Some(self.color),
153            border: Default::default(),
154            corner_radii: Default::default(),
155        })
156    }
157}
158
159///The GPUI element that paints the terminal.
160///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
161pub struct TerminalElement {
162    terminal: WeakModelHandle<Terminal>,
163    focused: bool,
164    cursor_visible: bool,
165    can_navigate_to_selected_word: bool,
166}
167
168impl TerminalElement {
169    pub fn new(
170        terminal: WeakModelHandle<Terminal>,
171        focused: bool,
172        cursor_visible: bool,
173        can_navigate_to_selected_word: bool,
174    ) -> TerminalElement {
175        TerminalElement {
176            terminal,
177            focused,
178            cursor_visible,
179            can_navigate_to_selected_word,
180        }
181    }
182
183    //Vec<Range<Point>> -> Clip out the parts of the ranges
184
185    fn layout_grid(
186        grid: &Vec<IndexedCell>,
187        text_style: &TextStyle,
188        terminal_theme: &TerminalStyle,
189        text_layout_cache: &TextLayoutCache,
190        font_cache: &FontCache,
191        hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
192    ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
193        let mut cells = vec![];
194        let mut rects = vec![];
195
196        let mut cur_rect: Option<LayoutRect> = None;
197        let mut cur_alac_color = None;
198
199        let linegroups = grid.into_iter().group_by(|i| i.point.line);
200        for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
201            for cell in line {
202                let mut fg = cell.fg;
203                let mut bg = cell.bg;
204                if cell.flags.contains(Flags::INVERSE) {
205                    mem::swap(&mut fg, &mut bg);
206                }
207
208                //Expand background rect range
209                {
210                    if matches!(bg, Named(NamedColor::Background)) {
211                        //Continue to next cell, resetting variables if necessary
212                        cur_alac_color = None;
213                        if let Some(rect) = cur_rect {
214                            rects.push(rect);
215                            cur_rect = None
216                        }
217                    } else {
218                        match cur_alac_color {
219                            Some(cur_color) => {
220                                if bg == cur_color {
221                                    cur_rect = cur_rect.take().map(|rect| rect.extend());
222                                } else {
223                                    cur_alac_color = Some(bg);
224                                    if cur_rect.is_some() {
225                                        rects.push(cur_rect.take().unwrap());
226                                    }
227                                    cur_rect = Some(LayoutRect::new(
228                                        Point::new(line_index as i32, cell.point.column.0 as i32),
229                                        1,
230                                        convert_color(&bg, &terminal_theme),
231                                    ));
232                                }
233                            }
234                            None => {
235                                cur_alac_color = Some(bg);
236                                cur_rect = Some(LayoutRect::new(
237                                    Point::new(line_index as i32, cell.point.column.0 as i32),
238                                    1,
239                                    convert_color(&bg, &terminal_theme),
240                                ));
241                            }
242                        }
243                    }
244                }
245
246                //Layout current cell text
247                {
248                    let cell_text = &cell.c.to_string();
249                    if !is_blank(&cell) {
250                        let cell_style = TerminalElement::cell_style(
251                            &cell,
252                            fg,
253                            terminal_theme,
254                            text_style,
255                            font_cache,
256                            hyperlink,
257                        );
258
259                        let layout_cell = text_layout_cache.layout_str(
260                            cell_text,
261                            text_style.font_size,
262                            &[(cell_text.len(), cell_style)],
263                        );
264
265                        cells.push(LayoutCell::new(
266                            Point::new(line_index as i32, cell.point.column.0 as i32),
267                            layout_cell,
268                        ))
269                    };
270                }
271            }
272
273            if cur_rect.is_some() {
274                rects.push(cur_rect.take().unwrap());
275            }
276        }
277        (cells, rects)
278    }
279
280    // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
281    // the same position for sequential indexes. Use em_width instead
282    fn shape_cursor(
283        cursor_point: DisplayCursor,
284        size: TerminalSize,
285        text_fragment: &Line,
286    ) -> Option<(Vector2F, f32)> {
287        if cursor_point.line() < size.total_lines() as i32 {
288            let cursor_width = if text_fragment.width() == 0. {
289                size.cell_width()
290            } else {
291                text_fragment.width()
292            };
293
294            //Cursor should always surround as much of the text as possible,
295            //hence when on pixel boundaries round the origin down and the width up
296            Some((
297                vec2f(
298                    (cursor_point.col() as f32 * size.cell_width()).floor(),
299                    (cursor_point.line() as f32 * size.line_height()).floor(),
300                ),
301                cursor_width.ceil(),
302            ))
303        } else {
304            None
305        }
306    }
307
308    ///Convert the Alacritty cell styles to GPUI text styles and background color
309    fn cell_style(
310        indexed: &IndexedCell,
311        fg: terminal::alacritty_terminal::ansi::Color,
312        style: &TerminalStyle,
313        text_style: &TextStyle,
314        font_cache: &FontCache,
315        hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
316    ) -> RunStyle {
317        let flags = indexed.cell.flags;
318        let fg = convert_color(&fg, &style);
319
320        let mut underline = flags
321            .intersects(Flags::ALL_UNDERLINES)
322            .then(|| Underline {
323                color: Some(fg),
324                squiggly: flags.contains(Flags::UNDERCURL),
325                thickness: OrderedFloat(1.),
326            })
327            .unwrap_or_default();
328
329        if indexed.cell.hyperlink().is_some() {
330            if underline.thickness == OrderedFloat(0.) {
331                underline.thickness = OrderedFloat(1.);
332            }
333        }
334
335        let mut properties = Properties::new();
336        if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
337            properties = *properties.weight(Weight::BOLD);
338        }
339        if indexed.flags.intersects(Flags::ITALIC) {
340            properties = *properties.style(Italic);
341        }
342
343        let font_id = font_cache
344            .select_font(text_style.font_family_id, &properties)
345            .unwrap_or(text_style.font_id);
346
347        let mut result = RunStyle {
348            color: fg,
349            font_id,
350            underline,
351        };
352
353        if let Some((style, range)) = hyperlink {
354            if range.contains(&indexed.point) {
355                if let Some(underline) = style.underline {
356                    result.underline = underline;
357                }
358
359                if let Some(color) = style.color {
360                    result.color = color;
361                }
362            }
363        }
364
365        result
366    }
367
368    fn generic_button_handler<E>(
369        connection: WeakModelHandle<Terminal>,
370        origin: Vector2F,
371        f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
372    ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
373        move |event, _: &mut TerminalView, cx| {
374            cx.focus_parent();
375            if let Some(conn_handle) = connection.upgrade(cx) {
376                conn_handle.update(cx, |terminal, cx| {
377                    f(terminal, origin, event, cx);
378
379                    cx.notify();
380                })
381            }
382        }
383    }
384
385    fn attach_mouse_handlers(
386        &self,
387        origin: Vector2F,
388        visible_bounds: RectF,
389        mode: TermMode,
390        cx: &mut ViewContext<TerminalView>,
391    ) {
392        let connection = self.terminal;
393
394        let mut region = MouseRegion::new::<Self>(cx.view_id(), 0, visible_bounds);
395
396        // Terminal Emulator controlled behavior:
397        region = region
398            // Start selections
399            .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
400                let terminal_view = cx.handle();
401                cx.focus(&terminal_view);
402                v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
403                if let Some(conn_handle) = connection.upgrade(cx) {
404                    conn_handle.update(cx, |terminal, cx| {
405                        terminal.mouse_down(&event, origin);
406
407                        cx.notify();
408                    })
409                }
410            })
411            // Update drag selections
412            .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
413                if event.end {
414                    return;
415                }
416
417                if cx.is_self_focused() {
418                    if let Some(conn_handle) = connection.upgrade(cx) {
419                        conn_handle.update(cx, |terminal, cx| {
420                            terminal.mouse_drag(event, origin);
421                            cx.notify();
422                        })
423                    }
424                }
425            })
426            // Copy on up behavior
427            .on_up(
428                MouseButton::Left,
429                TerminalElement::generic_button_handler(
430                    connection,
431                    origin,
432                    move |terminal, origin, e, cx| {
433                        terminal.mouse_up(&e, origin, cx);
434                    },
435                ),
436            )
437            // Context menu
438            .on_click(
439                MouseButton::Right,
440                move |event, view: &mut TerminalView, cx| {
441                    let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx) {
442                        conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift))
443                    } else {
444                        // If we can't get the model handle, probably can't deploy the context menu
445                        true
446                    };
447                    if !mouse_mode {
448                        view.deploy_context_menu(event.position, cx);
449                    }
450                },
451            )
452            .on_move(move |event, _: &mut TerminalView, cx| {
453                if cx.is_self_focused() {
454                    if let Some(conn_handle) = connection.upgrade(cx) {
455                        conn_handle.update(cx, |terminal, cx| {
456                            terminal.mouse_move(&event, origin);
457                            cx.notify();
458                        })
459                    }
460                }
461            })
462            .on_scroll(move |event, _: &mut TerminalView, cx| {
463                if let Some(conn_handle) = connection.upgrade(cx) {
464                    conn_handle.update(cx, |terminal, cx| {
465                        terminal.scroll_wheel(event, origin);
466                        cx.notify();
467                    })
468                }
469            });
470
471        // Mouse mode handlers:
472        // All mouse modes need the extra click handlers
473        if mode.intersects(TermMode::MOUSE_MODE) {
474            region = region
475                .on_down(
476                    MouseButton::Right,
477                    TerminalElement::generic_button_handler(
478                        connection,
479                        origin,
480                        move |terminal, origin, e, _cx| {
481                            terminal.mouse_down(&e, origin);
482                        },
483                    ),
484                )
485                .on_down(
486                    MouseButton::Middle,
487                    TerminalElement::generic_button_handler(
488                        connection,
489                        origin,
490                        move |terminal, origin, e, _cx| {
491                            terminal.mouse_down(&e, origin);
492                        },
493                    ),
494                )
495                .on_up(
496                    MouseButton::Right,
497                    TerminalElement::generic_button_handler(
498                        connection,
499                        origin,
500                        move |terminal, origin, e, cx| {
501                            terminal.mouse_up(&e, origin, cx);
502                        },
503                    ),
504                )
505                .on_up(
506                    MouseButton::Middle,
507                    TerminalElement::generic_button_handler(
508                        connection,
509                        origin,
510                        move |terminal, origin, e, cx| {
511                            terminal.mouse_up(&e, origin, cx);
512                        },
513                    ),
514                )
515        }
516
517        cx.scene().push_mouse_region(region);
518    }
519}
520
521impl Element<TerminalView> for TerminalElement {
522    type LayoutState = LayoutState;
523    type PaintState = ();
524
525    fn layout(
526        &mut self,
527        constraint: gpui::SizeConstraint,
528        view: &mut TerminalView,
529        cx: &mut ViewContext<TerminalView>,
530    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
531        let settings = settings::get::<ThemeSettings>(cx);
532        let terminal_settings = settings::get::<TerminalSettings>(cx);
533
534        //Setup layout information
535        let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
536        let link_style = settings.theme.editor.link_definition;
537        let tooltip_style = settings.theme.tooltip.clone();
538
539        let font_cache = cx.font_cache();
540        let font_size = terminal_settings
541            .font_size(cx)
542            .unwrap_or(settings.buffer_font_size(cx));
543        let font_family_name = terminal_settings
544            .font_family
545            .as_ref()
546            .unwrap_or(&settings.buffer_font_family_name);
547        let font_features = terminal_settings
548            .font_features
549            .as_ref()
550            .unwrap_or(&settings.buffer_font_features);
551        let family_id = font_cache
552            .load_family(&[font_family_name], &font_features)
553            .log_err()
554            .unwrap_or(settings.buffer_font_family);
555        let font_id = font_cache
556            .select_font(family_id, &Default::default())
557            .unwrap();
558
559        let text_style = TextStyle {
560            color: settings.theme.editor.text_color,
561            font_family_id: family_id,
562            font_family_name: font_cache.family_name(family_id).unwrap(),
563            font_id,
564            font_size,
565            font_properties: Default::default(),
566            underline: Default::default(),
567            soft_wrap: false,
568        };
569        let selection_color = settings.theme.editor.selection.selection;
570        let match_color = settings.theme.search.match_background;
571        let gutter;
572        let dimensions = {
573            let line_height = text_style.font_size * terminal_settings.line_height.value();
574            let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
575            gutter = cell_width;
576
577            let size = constraint.max - vec2f(gutter, 0.);
578            TerminalSize::new(line_height, cell_width, size)
579        };
580
581        let search_matches = if let Some(terminal_model) = self.terminal.upgrade(cx) {
582            terminal_model.read(cx).matches.clone()
583        } else {
584            Default::default()
585        };
586
587        let background_color = terminal_theme.background;
588        let terminal_handle = self.terminal.upgrade(cx).unwrap();
589
590        let last_hovered_word = terminal_handle.update(cx, |terminal, cx| {
591            terminal.set_size(dimensions);
592            terminal.try_sync(cx);
593            if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() {
594                terminal.last_content.last_hovered_word.clone()
595            } else {
596                None
597            }
598        });
599
600        let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
601            let mut tooltip = Overlay::new(
602                Empty::new()
603                    .contained()
604                    .constrained()
605                    .with_width(dimensions.width())
606                    .with_height(dimensions.height())
607                    .with_tooltip::<TerminalElement>(
608                        hovered_word.id,
609                        hovered_word.word,
610                        None,
611                        tooltip_style,
612                        cx,
613                    ),
614            )
615            .with_position_mode(gpui::elements::OverlayPositionMode::Local)
616            .into_any();
617
618            tooltip.layout(
619                SizeConstraint::new(Vector2F::zero(), cx.window_size()),
620                view,
621                cx,
622            );
623            tooltip
624        });
625
626        let TerminalContent {
627            cells,
628            mode,
629            display_offset,
630            cursor_char,
631            selection,
632            cursor,
633            ..
634        } = { &terminal_handle.read(cx).last_content };
635
636        // searches, highlights to a single range representations
637        let mut relative_highlighted_ranges = Vec::new();
638        for search_match in search_matches {
639            relative_highlighted_ranges.push((search_match, match_color))
640        }
641        if let Some(selection) = selection {
642            relative_highlighted_ranges.push((selection.start..=selection.end, selection_color));
643        }
644
645        // then have that representation be converted to the appropriate highlight data structure
646
647        let (cells, rects) = TerminalElement::layout_grid(
648            cells,
649            &text_style,
650            &terminal_theme,
651            cx.text_layout_cache(),
652            cx.font_cache(),
653            last_hovered_word
654                .as_ref()
655                .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
656        );
657
658        //Layout cursor. Rectangle is used for IME, so we should lay it out even
659        //if we don't end up showing it.
660        let cursor = if let AlacCursorShape::Hidden = cursor.shape {
661            None
662        } else {
663            let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
664            let cursor_text = {
665                let str_trxt = cursor_char.to_string();
666
667                let color = if self.focused {
668                    terminal_theme.background
669                } else {
670                    terminal_theme.foreground
671                };
672
673                cx.text_layout_cache().layout_str(
674                    &str_trxt,
675                    text_style.font_size,
676                    &[(
677                        str_trxt.len(),
678                        RunStyle {
679                            font_id: text_style.font_id,
680                            color,
681                            underline: Default::default(),
682                        },
683                    )],
684                )
685            };
686
687            let focused = self.focused;
688            TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
689                move |(cursor_position, block_width)| {
690                    let (shape, text) = match cursor.shape {
691                        AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
692                        AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
693                        AlacCursorShape::Underline => (CursorShape::Underscore, None),
694                        AlacCursorShape::Beam => (CursorShape::Bar, None),
695                        AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
696                        //This case is handled in the if wrapping the whole cursor layout
697                        AlacCursorShape::Hidden => unreachable!(),
698                    };
699
700                    Cursor::new(
701                        cursor_position,
702                        block_width,
703                        dimensions.line_height,
704                        terminal_theme.cursor,
705                        shape,
706                        text,
707                    )
708                },
709            )
710        };
711
712        //Done!
713        (
714            constraint.max,
715            LayoutState {
716                cells,
717                cursor,
718                background_color,
719                size: dimensions,
720                rects,
721                relative_highlighted_ranges,
722                mode: *mode,
723                display_offset: *display_offset,
724                hyperlink_tooltip,
725                gutter,
726            },
727        )
728    }
729
730    fn paint(
731        &mut self,
732        bounds: RectF,
733        visible_bounds: RectF,
734        layout: &mut Self::LayoutState,
735        view: &mut TerminalView,
736        cx: &mut ViewContext<TerminalView>,
737    ) -> Self::PaintState {
738        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
739
740        //Setup element stuff
741        let clip_bounds = Some(visible_bounds);
742
743        cx.paint_layer(clip_bounds, |cx| {
744            let origin = bounds.origin() + vec2f(layout.gutter, 0.);
745
746            // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
747            self.attach_mouse_handlers(origin, visible_bounds, layout.mode, cx);
748
749            cx.scene().push_cursor_region(gpui::CursorRegion {
750                bounds,
751                style: if layout.hyperlink_tooltip.is_some() {
752                    CursorStyle::PointingHand
753                } else {
754                    CursorStyle::IBeam
755                },
756            });
757
758            cx.paint_layer(clip_bounds, |cx| {
759                //Start with a background color
760                cx.scene().push_quad(Quad {
761                    bounds: RectF::new(bounds.origin(), bounds.size()),
762                    background: Some(layout.background_color),
763                    border: Default::default(),
764                    corner_radii: Default::default(),
765                });
766
767                for rect in &layout.rects {
768                    rect.paint(origin, layout, view, cx);
769                }
770            });
771
772            //Draw Highlighted Backgrounds
773            cx.paint_layer(clip_bounds, |cx| {
774                for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
775                {
776                    if let Some((start_y, highlighted_range_lines)) =
777                        to_highlighted_range_lines(relative_highlighted_range, layout, origin)
778                    {
779                        let hr = HighlightedRange {
780                            start_y, //Need to change this
781                            line_height: layout.size.line_height,
782                            lines: highlighted_range_lines,
783                            color: color.clone(),
784                            //Copied from editor. TODO: move to theme or something
785                            corner_radius: 0.15 * layout.size.line_height,
786                        };
787                        hr.paint(bounds, cx);
788                    }
789                }
790            });
791
792            //Draw the text cells
793            cx.paint_layer(clip_bounds, |cx| {
794                for cell in &layout.cells {
795                    cell.paint(origin, layout, visible_bounds, view, cx);
796                }
797            });
798
799            //Draw cursor
800            if self.cursor_visible {
801                if let Some(cursor) = &layout.cursor {
802                    cx.paint_layer(clip_bounds, |cx| {
803                        cursor.paint(origin, cx);
804                    })
805                }
806            }
807
808            if let Some(element) = &mut layout.hyperlink_tooltip {
809                element.paint(origin, visible_bounds, view, cx)
810            }
811        });
812    }
813
814    fn metadata(&self) -> Option<&dyn std::any::Any> {
815        None
816    }
817
818    fn debug(
819        &self,
820        _: RectF,
821        _: &Self::LayoutState,
822        _: &Self::PaintState,
823        _: &TerminalView,
824        _: &gpui::ViewContext<TerminalView>,
825    ) -> gpui::serde_json::Value {
826        json!({
827            "type": "TerminalElement",
828        })
829    }
830
831    fn rect_for_text_range(
832        &self,
833        _: Range<usize>,
834        bounds: RectF,
835        _: RectF,
836        layout: &Self::LayoutState,
837        _: &Self::PaintState,
838        _: &TerminalView,
839        _: &gpui::ViewContext<TerminalView>,
840    ) -> Option<RectF> {
841        // Use the same origin that's passed to `Cursor::paint` in the paint
842        // method bove.
843        let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
844
845        // TODO - Why is it necessary to move downward one line to get correct
846        // positioning? I would think that we'd want the same rect that is
847        // painted for the cursor.
848        origin += vec2f(0., layout.size.line_height);
849
850        Some(layout.cursor.as_ref()?.bounding_rect(origin))
851    }
852}
853
854fn is_blank(cell: &IndexedCell) -> bool {
855    if cell.c != ' ' {
856        return false;
857    }
858
859    if cell.bg != AnsiColor::Named(NamedColor::Background) {
860        return false;
861    }
862
863    if cell.hyperlink().is_some() {
864        return false;
865    }
866
867    if cell
868        .flags
869        .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
870    {
871        return false;
872    }
873
874    return true;
875}
876
877fn to_highlighted_range_lines(
878    range: &RangeInclusive<Point>,
879    layout: &LayoutState,
880    origin: Vector2F,
881) -> Option<(f32, Vec<HighlightedRangeLine>)> {
882    // Step 1. Normalize the points to be viewport relative.
883    // When display_offset = 1, here's how the grid is arranged:
884    //-2,0 -2,1...
885    //--- Viewport top
886    //-1,0 -1,1...
887    //--------- Terminal Top
888    // 0,0  0,1...
889    // 1,0  1,1...
890    //--- Viewport Bottom
891    // 2,0  2,1...
892    //--------- Terminal Bottom
893
894    // Normalize to viewport relative, from terminal relative.
895    // lines are i32s, which are negative above the top left corner of the terminal
896    // If the user has scrolled, we use the display_offset to tell us which offset
897    // of the grid data we should be looking at. But for the rendering step, we don't
898    // want negatives. We want things relative to the 'viewport' (the area of the grid
899    // which is currently shown according to the display offset)
900    let unclamped_start = Point::new(
901        range.start().line + layout.display_offset,
902        range.start().column,
903    );
904    let unclamped_end = Point::new(range.end().line + layout.display_offset, range.end().column);
905
906    // Step 2. Clamp range to viewport, and return None if it doesn't overlap
907    if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 {
908        return None;
909    }
910
911    let clamped_start_line = unclamped_start.line.0.max(0) as usize;
912    let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize;
913    //Convert the start of the range to pixels
914    let start_y = origin.y() + clamped_start_line as f32 * layout.size.line_height;
915
916    // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
917    //  (also convert to pixels)
918    let mut highlighted_range_lines = Vec::new();
919    for line in clamped_start_line..=clamped_end_line {
920        let mut line_start = 0;
921        let mut line_end = layout.size.columns();
922
923        if line == clamped_start_line {
924            line_start = unclamped_start.column.0 as usize;
925        }
926        if line == clamped_end_line {
927            line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive
928        }
929
930        highlighted_range_lines.push(HighlightedRangeLine {
931            start_x: origin.x() + line_start as f32 * layout.size.cell_width,
932            end_x: origin.x() + line_end as f32 * layout.size.cell_width,
933        });
934    }
935
936    Some((start_y, highlighted_range_lines))
937}