terminal_element.rs

   1use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
   2use gpui::{
   3    div, fill, point, px, relative, AnyElement, Bounds, DispatchPhase, Element, ElementContext,
   4    FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hitbox, Hsla, InputHandler,
   5    InteractiveElement, Interactivity, IntoElement, LayoutId, Model, ModelContext,
   6    ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine,
   7    StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UnderlineStyle,
   8    WeakView, WhiteSpace, WindowContext, WindowTextSystem,
   9};
  10use itertools::Itertools;
  11use language::CursorShape;
  12use settings::Settings;
  13use terminal::{
  14    alacritty_terminal::{
  15        grid::Dimensions,
  16        index::Point as AlacPoint,
  17        term::{cell::Flags, TermMode},
  18        vte::ansi::{
  19            Color::{self as AnsiColor, Named},
  20            CursorShape as AlacCursorShape, NamedColor,
  21        },
  22    },
  23    terminal_settings::TerminalSettings,
  24    HoveredWord, IndexedCell, Terminal, TerminalContent, TerminalSize,
  25};
  26use theme::{ActiveTheme, Theme, ThemeSettings};
  27use ui::Tooltip;
  28use workspace::Workspace;
  29
  30use std::mem;
  31use std::{fmt::Debug, ops::RangeInclusive};
  32
  33/// The information generated during layout that is necessary for painting.
  34pub struct LayoutState {
  35    hitbox: Hitbox,
  36    cells: Vec<LayoutCell>,
  37    rects: Vec<LayoutRect>,
  38    relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
  39    cursor: Option<CursorLayout>,
  40    background_color: Hsla,
  41    dimensions: TerminalSize,
  42    mode: TermMode,
  43    display_offset: usize,
  44    hyperlink_tooltip: Option<AnyElement>,
  45    gutter: Pixels,
  46    last_hovered_word: Option<HoveredWord>,
  47}
  48
  49/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
  50struct DisplayCursor {
  51    line: i32,
  52    col: usize,
  53}
  54
  55impl DisplayCursor {
  56    fn from(cursor_point: AlacPoint, display_offset: usize) -> Self {
  57        Self {
  58            line: cursor_point.line.0 + display_offset as i32,
  59            col: cursor_point.column.0,
  60        }
  61    }
  62
  63    pub fn line(&self) -> i32 {
  64        self.line
  65    }
  66
  67    pub fn col(&self) -> usize {
  68        self.col
  69    }
  70}
  71
  72#[derive(Debug, Default)]
  73struct LayoutCell {
  74    point: AlacPoint<i32, i32>,
  75    text: gpui::ShapedLine,
  76}
  77
  78impl LayoutCell {
  79    fn new(point: AlacPoint<i32, i32>, text: gpui::ShapedLine) -> LayoutCell {
  80        LayoutCell { point, text }
  81    }
  82
  83    fn paint(
  84        &self,
  85        origin: Point<Pixels>,
  86        layout: &LayoutState,
  87        _visible_bounds: Bounds<Pixels>,
  88        cx: &mut ElementContext,
  89    ) {
  90        let pos = {
  91            let point = self.point;
  92
  93            Point::new(
  94                (origin.x + point.column as f32 * layout.dimensions.cell_width).floor(),
  95                origin.y + point.line as f32 * layout.dimensions.line_height,
  96            )
  97        };
  98
  99        self.text.paint(pos, layout.dimensions.line_height, cx).ok();
 100    }
 101}
 102
 103#[derive(Clone, Debug, Default)]
 104struct LayoutRect {
 105    point: AlacPoint<i32, i32>,
 106    num_of_cells: usize,
 107    color: Hsla,
 108}
 109
 110impl LayoutRect {
 111    fn new(point: AlacPoint<i32, i32>, num_of_cells: usize, color: Hsla) -> LayoutRect {
 112        LayoutRect {
 113            point,
 114            num_of_cells,
 115            color,
 116        }
 117    }
 118
 119    fn extend(&self) -> Self {
 120        LayoutRect {
 121            point: self.point,
 122            num_of_cells: self.num_of_cells + 1,
 123            color: self.color,
 124        }
 125    }
 126
 127    fn paint(&self, origin: Point<Pixels>, layout: &LayoutState, cx: &mut ElementContext) {
 128        let position = {
 129            let alac_point = self.point;
 130            point(
 131                (origin.x + alac_point.column as f32 * layout.dimensions.cell_width).floor(),
 132                origin.y + alac_point.line as f32 * layout.dimensions.line_height,
 133            )
 134        };
 135        let size = point(
 136            (layout.dimensions.cell_width * self.num_of_cells as f32).ceil(),
 137            layout.dimensions.line_height,
 138        )
 139        .into();
 140
 141        cx.paint_quad(fill(Bounds::new(position, size), self.color));
 142    }
 143}
 144
 145/// The GPUI element that paints the terminal.
 146/// 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?
 147pub struct TerminalElement {
 148    terminal: Model<Terminal>,
 149    workspace: WeakView<Workspace>,
 150    focus: FocusHandle,
 151    focused: bool,
 152    cursor_visible: bool,
 153    can_navigate_to_selected_word: bool,
 154    interactivity: Interactivity,
 155}
 156
 157impl InteractiveElement for TerminalElement {
 158    fn interactivity(&mut self) -> &mut Interactivity {
 159        &mut self.interactivity
 160    }
 161}
 162
 163impl StatefulInteractiveElement for TerminalElement {}
 164
 165impl TerminalElement {
 166    pub fn new(
 167        terminal: Model<Terminal>,
 168        workspace: WeakView<Workspace>,
 169        focus: FocusHandle,
 170        focused: bool,
 171        cursor_visible: bool,
 172        can_navigate_to_selected_word: bool,
 173    ) -> TerminalElement {
 174        TerminalElement {
 175            terminal,
 176            workspace,
 177            focused,
 178            focus: focus.clone(),
 179            cursor_visible,
 180            can_navigate_to_selected_word,
 181            interactivity: Default::default(),
 182        }
 183        .track_focus(&focus)
 184        .element
 185    }
 186
 187    //Vec<Range<AlacPoint>> -> Clip out the parts of the ranges
 188
 189    fn layout_grid(
 190        grid: &Vec<IndexedCell>,
 191        text_style: &TextStyle,
 192        // terminal_theme: &TerminalStyle,
 193        text_system: &WindowTextSystem,
 194        hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
 195        cx: &WindowContext<'_>,
 196    ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
 197        let theme = cx.theme();
 198        let mut cells = vec![];
 199        let mut rects = vec![];
 200
 201        let mut cur_rect: Option<LayoutRect> = None;
 202        let mut cur_alac_color = None;
 203
 204        let linegroups = grid.into_iter().group_by(|i| i.point.line);
 205        for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
 206            for cell in line {
 207                let mut fg = cell.fg;
 208                let mut bg = cell.bg;
 209                if cell.flags.contains(Flags::INVERSE) {
 210                    mem::swap(&mut fg, &mut bg);
 211                }
 212
 213                //Expand background rect range
 214                {
 215                    if matches!(bg, Named(NamedColor::Background)) {
 216                        //Continue to next cell, resetting variables if necessary
 217                        cur_alac_color = None;
 218                        if let Some(rect) = cur_rect {
 219                            rects.push(rect);
 220                            cur_rect = None
 221                        }
 222                    } else {
 223                        match cur_alac_color {
 224                            Some(cur_color) => {
 225                                if bg == cur_color {
 226                                    // `cur_rect` can be None if it was moved to the `rects` vec after wrapping around
 227                                    // from one line to the next. The variables are all set correctly but there is no current
 228                                    // rect, so we create one if necessary.
 229                                    cur_rect = cur_rect.map_or_else(
 230                                        || {
 231                                            Some(LayoutRect::new(
 232                                                AlacPoint::new(
 233                                                    line_index as i32,
 234                                                    cell.point.column.0 as i32,
 235                                                ),
 236                                                1,
 237                                                convert_color(&bg, theme),
 238                                            ))
 239                                        },
 240                                        |rect| Some(rect.extend()),
 241                                    );
 242                                } else {
 243                                    cur_alac_color = Some(bg);
 244                                    if cur_rect.is_some() {
 245                                        rects.push(cur_rect.take().unwrap());
 246                                    }
 247                                    cur_rect = Some(LayoutRect::new(
 248                                        AlacPoint::new(
 249                                            line_index as i32,
 250                                            cell.point.column.0 as i32,
 251                                        ),
 252                                        1,
 253                                        convert_color(&bg, theme),
 254                                    ));
 255                                }
 256                            }
 257                            None => {
 258                                cur_alac_color = Some(bg);
 259                                cur_rect = Some(LayoutRect::new(
 260                                    AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
 261                                    1,
 262                                    convert_color(&bg, &theme),
 263                                ));
 264                            }
 265                        }
 266                    }
 267                }
 268
 269                //Layout current cell text
 270                {
 271                    if !is_blank(&cell) {
 272                        let cell_text = cell.c.to_string();
 273                        let cell_style =
 274                            TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink);
 275
 276                        let layout_cell = text_system
 277                            .shape_line(
 278                                cell_text.into(),
 279                                text_style.font_size.to_pixels(cx.rem_size()),
 280                                &[cell_style],
 281                            )
 282                            .unwrap();
 283
 284                        cells.push(LayoutCell::new(
 285                            AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
 286                            layout_cell,
 287                        ))
 288                    };
 289                }
 290            }
 291
 292            if cur_rect.is_some() {
 293                rects.push(cur_rect.take().unwrap());
 294            }
 295        }
 296        (cells, rects)
 297    }
 298
 299    /// Computes the cursor position and expected block width, may return a zero width if x_for_index returns
 300    /// the same position for sequential indexes. Use em_width instead
 301    fn shape_cursor(
 302        cursor_point: DisplayCursor,
 303        size: TerminalSize,
 304        text_fragment: &ShapedLine,
 305    ) -> Option<(Point<Pixels>, Pixels)> {
 306        if cursor_point.line() < size.total_lines() as i32 {
 307            let cursor_width = if text_fragment.width == Pixels::ZERO {
 308                size.cell_width()
 309            } else {
 310                text_fragment.width
 311            };
 312
 313            // Cursor should always surround as much of the text as possible,
 314            // hence when on pixel boundaries round the origin down and the width up
 315            Some((
 316                point(
 317                    (cursor_point.col() as f32 * size.cell_width()).floor(),
 318                    (cursor_point.line() as f32 * size.line_height()).floor(),
 319                ),
 320                cursor_width.ceil(),
 321            ))
 322        } else {
 323            None
 324        }
 325    }
 326
 327    /// Converts the Alacritty cell styles to GPUI text styles and background color.
 328    fn cell_style(
 329        indexed: &IndexedCell,
 330        fg: terminal::alacritty_terminal::vte::ansi::Color,
 331        // bg: terminal::alacritty_terminal::ansi::Color,
 332        colors: &Theme,
 333        text_style: &TextStyle,
 334        hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
 335    ) -> TextRun {
 336        let flags = indexed.cell.flags;
 337        let mut fg = convert_color(&fg, &colors);
 338
 339        // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty
 340        // uses 0.75. We're using 0.7 because it's pretty well in the middle of that.
 341        if flags.intersects(Flags::DIM) {
 342            fg.a *= 0.7;
 343        }
 344
 345        let underline = (flags.intersects(Flags::ALL_UNDERLINES)
 346            || indexed.cell.hyperlink().is_some())
 347        .then(|| UnderlineStyle {
 348            color: Some(fg),
 349            thickness: Pixels::from(1.0),
 350            wavy: flags.contains(Flags::UNDERCURL),
 351        });
 352
 353        let strikethrough = flags
 354            .intersects(Flags::STRIKEOUT)
 355            .then(|| StrikethroughStyle {
 356                color: Some(fg),
 357                thickness: Pixels::from(1.0),
 358            });
 359
 360        let weight = if flags.intersects(Flags::BOLD) {
 361            FontWeight::BOLD
 362        } else {
 363            FontWeight::NORMAL
 364        };
 365
 366        let style = if flags.intersects(Flags::ITALIC) {
 367            FontStyle::Italic
 368        } else {
 369            FontStyle::Normal
 370        };
 371
 372        let mut result = TextRun {
 373            len: indexed.c.len_utf8(),
 374            color: fg,
 375            background_color: None,
 376            font: Font {
 377                weight,
 378                style,
 379                ..text_style.font()
 380            },
 381            underline,
 382            strikethrough,
 383        };
 384
 385        if let Some((style, range)) = hyperlink {
 386            if range.contains(&indexed.point) {
 387                if let Some(underline) = style.underline {
 388                    result.underline = Some(underline);
 389                }
 390
 391                if let Some(color) = style.color {
 392                    result.color = color;
 393                }
 394            }
 395        }
 396
 397        result
 398    }
 399
 400    fn generic_button_handler<E>(
 401        connection: Model<Terminal>,
 402        origin: Point<Pixels>,
 403        focus_handle: FocusHandle,
 404        f: impl Fn(&mut Terminal, Point<Pixels>, &E, &mut ModelContext<Terminal>),
 405    ) -> impl Fn(&E, &mut WindowContext) {
 406        move |event, cx| {
 407            cx.focus(&focus_handle);
 408            connection.update(cx, |terminal, cx| {
 409                f(terminal, origin, event, cx);
 410
 411                cx.notify();
 412            })
 413        }
 414    }
 415
 416    fn register_mouse_listeners(
 417        &mut self,
 418        origin: Point<Pixels>,
 419        mode: TermMode,
 420        hitbox: &Hitbox,
 421        cx: &mut ElementContext,
 422    ) {
 423        let focus = self.focus.clone();
 424        let terminal = self.terminal.clone();
 425
 426        self.interactivity.on_mouse_down(MouseButton::Left, {
 427            let terminal = terminal.clone();
 428            let focus = focus.clone();
 429            move |e, cx| {
 430                cx.focus(&focus);
 431                terminal.update(cx, |terminal, cx| {
 432                    terminal.mouse_down(&e, origin);
 433                    cx.notify();
 434                })
 435            }
 436        });
 437
 438        cx.on_mouse_event({
 439            let focus = self.focus.clone();
 440            let terminal = self.terminal.clone();
 441            let hitbox = hitbox.clone();
 442            move |e: &MouseMoveEvent, phase, cx| {
 443                if phase != DispatchPhase::Bubble || !focus.is_focused(cx) {
 444                    return;
 445                }
 446
 447                if e.pressed_button.is_some() && !cx.has_active_drag() {
 448                    let hovered = hitbox.is_hovered(cx);
 449                    terminal.update(cx, |terminal, cx| {
 450                        if !terminal.selection_started() {
 451                            if hovered {
 452                                terminal.mouse_drag(e, origin, hitbox.bounds);
 453                                cx.notify();
 454                            }
 455                        } else {
 456                            terminal.mouse_drag(e, origin, hitbox.bounds);
 457                            cx.notify();
 458                        }
 459                    })
 460                }
 461
 462                if hitbox.is_hovered(cx) {
 463                    terminal.update(cx, |terminal, cx| {
 464                        terminal.mouse_move(&e, origin);
 465                        cx.notify();
 466                    })
 467                }
 468            }
 469        });
 470
 471        self.interactivity.on_mouse_up(
 472            MouseButton::Left,
 473            TerminalElement::generic_button_handler(
 474                terminal.clone(),
 475                origin,
 476                focus.clone(),
 477                move |terminal, origin, e, cx| {
 478                    terminal.mouse_up(&e, origin, cx);
 479                },
 480            ),
 481        );
 482        self.interactivity.on_scroll_wheel({
 483            let terminal = terminal.clone();
 484            move |e, cx| {
 485                terminal.update(cx, |terminal, cx| {
 486                    terminal.scroll_wheel(e, origin);
 487                    cx.notify();
 488                })
 489            }
 490        });
 491
 492        // Mouse mode handlers:
 493        // All mouse modes need the extra click handlers
 494        if mode.intersects(TermMode::MOUSE_MODE) {
 495            self.interactivity.on_mouse_down(
 496                MouseButton::Right,
 497                TerminalElement::generic_button_handler(
 498                    terminal.clone(),
 499                    origin,
 500                    focus.clone(),
 501                    move |terminal, origin, e, _cx| {
 502                        terminal.mouse_down(&e, origin);
 503                    },
 504                ),
 505            );
 506            self.interactivity.on_mouse_down(
 507                MouseButton::Middle,
 508                TerminalElement::generic_button_handler(
 509                    terminal.clone(),
 510                    origin,
 511                    focus.clone(),
 512                    move |terminal, origin, e, _cx| {
 513                        terminal.mouse_down(&e, origin);
 514                    },
 515                ),
 516            );
 517            self.interactivity.on_mouse_up(
 518                MouseButton::Right,
 519                TerminalElement::generic_button_handler(
 520                    terminal.clone(),
 521                    origin,
 522                    focus.clone(),
 523                    move |terminal, origin, e, cx| {
 524                        terminal.mouse_up(&e, origin, cx);
 525                    },
 526                ),
 527            );
 528            self.interactivity.on_mouse_up(
 529                MouseButton::Middle,
 530                TerminalElement::generic_button_handler(
 531                    terminal,
 532                    origin,
 533                    focus,
 534                    move |terminal, origin, e, cx| {
 535                        terminal.mouse_up(&e, origin, cx);
 536                    },
 537                ),
 538            );
 539        }
 540    }
 541}
 542
 543impl Element for TerminalElement {
 544    type BeforeLayout = ();
 545    type AfterLayout = LayoutState;
 546
 547    fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
 548        self.interactivity.occlude_mouse();
 549        let layout_id = self.interactivity.before_layout(cx, |mut style, cx| {
 550            style.size.width = relative(1.).into();
 551            style.size.height = relative(1.).into();
 552            let layout_id = cx.request_layout(&style, None);
 553
 554            layout_id
 555        });
 556        (layout_id, ())
 557    }
 558
 559    fn after_layout(
 560        &mut self,
 561        bounds: Bounds<Pixels>,
 562        _: &mut Self::BeforeLayout,
 563        cx: &mut ElementContext,
 564    ) -> Self::AfterLayout {
 565        self.interactivity
 566            .after_layout(bounds, bounds.size, cx, |_, _, hitbox, cx| {
 567                let hitbox = hitbox.unwrap();
 568                let settings = ThemeSettings::get_global(cx).clone();
 569
 570                let buffer_font_size = settings.buffer_font_size(cx);
 571
 572                let terminal_settings = TerminalSettings::get_global(cx);
 573                let font_family = terminal_settings
 574                    .font_family
 575                    .as_ref()
 576                    .map(|string| string.clone().into())
 577                    .unwrap_or(settings.buffer_font.family);
 578
 579                let font_features = terminal_settings
 580                    .font_features
 581                    .unwrap_or(settings.buffer_font.features);
 582
 583                let line_height = terminal_settings.line_height.value();
 584                let font_size = terminal_settings.font_size;
 585
 586                let font_size =
 587                    font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx));
 588
 589                let theme = cx.theme().clone();
 590
 591                let link_style = HighlightStyle {
 592                    color: Some(theme.colors().link_text_hover),
 593                    font_weight: None,
 594                    font_style: None,
 595                    background_color: None,
 596                    underline: Some(UnderlineStyle {
 597                        thickness: px(1.0),
 598                        color: Some(theme.colors().link_text_hover),
 599                        wavy: false,
 600                    }),
 601                    strikethrough: None,
 602                    fade_out: None,
 603                };
 604
 605                let text_style = TextStyle {
 606                    font_family,
 607                    font_features,
 608                    font_size: font_size.into(),
 609                    font_style: FontStyle::Normal,
 610                    line_height: line_height.into(),
 611                    background_color: None,
 612                    white_space: WhiteSpace::Normal,
 613                    // These are going to be overridden per-cell
 614                    underline: None,
 615                    strikethrough: None,
 616                    color: theme.colors().text,
 617                    font_weight: FontWeight::NORMAL,
 618                };
 619
 620                let text_system = cx.text_system();
 621                let player_color = theme.players().local();
 622                let match_color = theme.colors().search_match_background;
 623                let gutter;
 624                let dimensions = {
 625                    let rem_size = cx.rem_size();
 626                    let font_pixels = text_style.font_size.to_pixels(rem_size);
 627                    let line_height = font_pixels * line_height.to_pixels(rem_size);
 628                    let font_id = cx.text_system().resolve_font(&text_style.font());
 629
 630                    let cell_width = text_system
 631                        .advance(font_id, font_pixels, 'm')
 632                        .unwrap()
 633                        .width;
 634                    gutter = cell_width;
 635
 636                    let mut size = bounds.size;
 637                    size.width -= gutter;
 638
 639                    // https://github.com/zed-industries/zed/issues/2750
 640                    // if the terminal is one column wide, rendering 🦀
 641                    // causes alacritty to misbehave.
 642                    if size.width < cell_width * 2.0 {
 643                        size.width = cell_width * 2.0;
 644                    }
 645
 646                    TerminalSize::new(line_height, cell_width, size)
 647                };
 648
 649                let search_matches = self.terminal.read(cx).matches.clone();
 650
 651                let background_color = theme.colors().terminal_background;
 652
 653                let last_hovered_word = self.terminal.update(cx, |terminal, cx| {
 654                    terminal.set_size(dimensions);
 655                    terminal.sync(cx);
 656                    if self.can_navigate_to_selected_word
 657                        && terminal.can_navigate_to_selected_word()
 658                    {
 659                        terminal.last_content.last_hovered_word.clone()
 660                    } else {
 661                        None
 662                    }
 663                });
 664
 665                let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
 666                    let offset = bounds.origin + Point::new(gutter, px(0.));
 667                    let mut element = div()
 668                        .size_full()
 669                        .id("terminal-element")
 670                        .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx))
 671                        .into_any_element();
 672                    element.layout(offset, bounds.size.into(), cx);
 673                    element
 674                });
 675
 676                let TerminalContent {
 677                    cells,
 678                    mode,
 679                    display_offset,
 680                    cursor_char,
 681                    selection,
 682                    cursor,
 683                    ..
 684                } = &self.terminal.read(cx).last_content;
 685
 686                // searches, highlights to a single range representations
 687                let mut relative_highlighted_ranges = Vec::new();
 688                for search_match in search_matches {
 689                    relative_highlighted_ranges.push((search_match, match_color))
 690                }
 691                if let Some(selection) = selection {
 692                    relative_highlighted_ranges
 693                        .push((selection.start..=selection.end, player_color.selection));
 694                }
 695
 696                // then have that representation be converted to the appropriate highlight data structure
 697
 698                let (cells, rects) = TerminalElement::layout_grid(
 699                    cells,
 700                    &text_style,
 701                    &cx.text_system(),
 702                    last_hovered_word
 703                        .as_ref()
 704                        .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
 705                    cx,
 706                );
 707
 708                // Layout cursor. Rectangle is used for IME, so we should lay it out even
 709                // if we don't end up showing it.
 710                let cursor = if let AlacCursorShape::Hidden = cursor.shape {
 711                    None
 712                } else {
 713                    let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
 714                    let cursor_text = {
 715                        let str_trxt = cursor_char.to_string();
 716                        let len = str_trxt.len();
 717                        cx.text_system()
 718                            .shape_line(
 719                                str_trxt.into(),
 720                                text_style.font_size.to_pixels(cx.rem_size()),
 721                                &[TextRun {
 722                                    len,
 723                                    font: text_style.font(),
 724                                    color: theme.colors().terminal_background,
 725                                    background_color: None,
 726                                    underline: Default::default(),
 727                                    strikethrough: None,
 728                                }],
 729                            )
 730                            .unwrap()
 731                    };
 732
 733                    let focused = self.focused;
 734                    TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
 735                        move |(cursor_position, block_width)| {
 736                            let (shape, text) = match cursor.shape {
 737                                AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
 738                                AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
 739                                AlacCursorShape::Underline => (CursorShape::Underscore, None),
 740                                AlacCursorShape::Beam => (CursorShape::Bar, None),
 741                                AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
 742                                //This case is handled in the if wrapping the whole cursor layout
 743                                AlacCursorShape::Hidden => unreachable!(),
 744                            };
 745
 746                            CursorLayout::new(
 747                                cursor_position,
 748                                block_width,
 749                                dimensions.line_height,
 750                                theme.players().local().cursor,
 751                                shape,
 752                                text,
 753                            )
 754                        },
 755                    )
 756                };
 757
 758                LayoutState {
 759                    hitbox,
 760                    cells,
 761                    cursor,
 762                    background_color,
 763                    dimensions,
 764                    rects,
 765                    relative_highlighted_ranges,
 766                    mode: *mode,
 767                    display_offset: *display_offset,
 768                    hyperlink_tooltip,
 769                    gutter,
 770                    last_hovered_word,
 771                }
 772            })
 773    }
 774
 775    fn paint(
 776        &mut self,
 777        bounds: Bounds<Pixels>,
 778        _: &mut Self::BeforeLayout,
 779        layout: &mut Self::AfterLayout,
 780        cx: &mut ElementContext<'_>,
 781    ) {
 782        cx.paint_quad(fill(bounds, layout.background_color));
 783        let origin = bounds.origin + Point::new(layout.gutter, px(0.));
 784
 785        let terminal_input_handler = TerminalInputHandler {
 786            terminal: self.terminal.clone(),
 787            cursor_bounds: layout
 788                .cursor
 789                .as_ref()
 790                .map(|cursor| cursor.bounding_rect(origin)),
 791            workspace: self.workspace.clone(),
 792        };
 793
 794        self.register_mouse_listeners(origin, layout.mode, &layout.hitbox, cx);
 795        if self.can_navigate_to_selected_word && layout.last_hovered_word.is_some() {
 796            cx.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
 797        } else {
 798            cx.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
 799        }
 800
 801        let cursor = layout.cursor.take();
 802        let hyperlink_tooltip = layout.hyperlink_tooltip.take();
 803        self.interactivity
 804            .paint(bounds, Some(&layout.hitbox), cx, |_, cx| {
 805                cx.handle_input(&self.focus, terminal_input_handler);
 806
 807                cx.on_key_event({
 808                    let this = self.terminal.clone();
 809                    move |event: &ModifiersChangedEvent, phase, cx| {
 810                        if phase != DispatchPhase::Bubble {
 811                            return;
 812                        }
 813
 814                        let handled =
 815                            this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
 816
 817                        if handled {
 818                            cx.refresh();
 819                        }
 820                    }
 821                });
 822
 823                for rect in &layout.rects {
 824                    rect.paint(origin, &layout, cx);
 825                }
 826
 827                for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
 828                {
 829                    if let Some((start_y, highlighted_range_lines)) =
 830                        to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
 831                    {
 832                        let hr = HighlightedRange {
 833                            start_y, //Need to change this
 834                            line_height: layout.dimensions.line_height,
 835                            lines: highlighted_range_lines,
 836                            color: *color,
 837                            //Copied from editor. TODO: move to theme or something
 838                            corner_radius: 0.15 * layout.dimensions.line_height,
 839                        };
 840                        hr.paint(bounds, cx);
 841                    }
 842                }
 843
 844                for cell in &layout.cells {
 845                    cell.paint(origin, &layout, bounds, cx);
 846                }
 847
 848                if self.cursor_visible {
 849                    if let Some(mut cursor) = cursor {
 850                        cursor.paint(origin, cx);
 851                    }
 852                }
 853
 854                if let Some(mut element) = hyperlink_tooltip {
 855                    element.paint(cx);
 856                }
 857            });
 858    }
 859}
 860
 861impl IntoElement for TerminalElement {
 862    type Element = Self;
 863
 864    fn into_element(self) -> Self::Element {
 865        self
 866    }
 867}
 868
 869struct TerminalInputHandler {
 870    terminal: Model<Terminal>,
 871    workspace: WeakView<Workspace>,
 872    cursor_bounds: Option<Bounds<Pixels>>,
 873}
 874
 875impl InputHandler for TerminalInputHandler {
 876    fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<std::ops::Range<usize>> {
 877        if self
 878            .terminal
 879            .read(cx)
 880            .last_content
 881            .mode
 882            .contains(TermMode::ALT_SCREEN)
 883        {
 884            None
 885        } else {
 886            Some(0..0)
 887        }
 888    }
 889
 890    fn marked_text_range(&mut self, _: &mut WindowContext) -> Option<std::ops::Range<usize>> {
 891        None
 892    }
 893
 894    fn text_for_range(
 895        &mut self,
 896        _: std::ops::Range<usize>,
 897        _: &mut WindowContext,
 898    ) -> Option<String> {
 899        None
 900    }
 901
 902    fn replace_text_in_range(
 903        &mut self,
 904        _replacement_range: Option<std::ops::Range<usize>>,
 905        text: &str,
 906        cx: &mut WindowContext,
 907    ) {
 908        self.terminal.update(cx, |terminal, _| {
 909            terminal.input(text.into());
 910        });
 911
 912        self.workspace
 913            .update(cx, |this, cx| {
 914                let telemetry = this.project().read(cx).client().telemetry().clone();
 915                telemetry.log_edit_event("terminal");
 916            })
 917            .ok();
 918    }
 919
 920    fn replace_and_mark_text_in_range(
 921        &mut self,
 922        _range_utf16: Option<std::ops::Range<usize>>,
 923        _new_text: &str,
 924        _new_selected_range: Option<std::ops::Range<usize>>,
 925        _: &mut WindowContext,
 926    ) {
 927    }
 928
 929    fn unmark_text(&mut self, _: &mut WindowContext) {}
 930
 931    fn bounds_for_range(
 932        &mut self,
 933        _range_utf16: std::ops::Range<usize>,
 934        _: &mut WindowContext,
 935    ) -> Option<Bounds<Pixels>> {
 936        self.cursor_bounds
 937    }
 938}
 939
 940fn is_blank(cell: &IndexedCell) -> bool {
 941    if cell.c != ' ' {
 942        return false;
 943    }
 944
 945    if cell.bg != AnsiColor::Named(NamedColor::Background) {
 946        return false;
 947    }
 948
 949    if cell.hyperlink().is_some() {
 950        return false;
 951    }
 952
 953    if cell
 954        .flags
 955        .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
 956    {
 957        return false;
 958    }
 959
 960    return true;
 961}
 962
 963fn to_highlighted_range_lines(
 964    range: &RangeInclusive<AlacPoint>,
 965    layout: &LayoutState,
 966    origin: Point<Pixels>,
 967) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
 968    // Step 1. Normalize the points to be viewport relative.
 969    // When display_offset = 1, here's how the grid is arranged:
 970    //-2,0 -2,1...
 971    //--- Viewport top
 972    //-1,0 -1,1...
 973    //--------- Terminal Top
 974    // 0,0  0,1...
 975    // 1,0  1,1...
 976    //--- Viewport Bottom
 977    // 2,0  2,1...
 978    //--------- Terminal Bottom
 979
 980    // Normalize to viewport relative, from terminal relative.
 981    // lines are i32s, which are negative above the top left corner of the terminal
 982    // If the user has scrolled, we use the display_offset to tell us which offset
 983    // of the grid data we should be looking at. But for the rendering step, we don't
 984    // want negatives. We want things relative to the 'viewport' (the area of the grid
 985    // which is currently shown according to the display offset)
 986    let unclamped_start = AlacPoint::new(
 987        range.start().line + layout.display_offset,
 988        range.start().column,
 989    );
 990    let unclamped_end =
 991        AlacPoint::new(range.end().line + layout.display_offset, range.end().column);
 992
 993    // Step 2. Clamp range to viewport, and return None if it doesn't overlap
 994    if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 {
 995        return None;
 996    }
 997
 998    let clamped_start_line = unclamped_start.line.0.max(0) as usize;
 999    let clamped_end_line = unclamped_end
1000        .line
1001        .0
1002        .min(layout.dimensions.num_lines() as i32) as usize;
1003    //Convert the start of the range to pixels
1004    let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height;
1005
1006    // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
1007    //  (also convert to pixels)
1008    let mut highlighted_range_lines = Vec::new();
1009    for line in clamped_start_line..=clamped_end_line {
1010        let mut line_start = 0;
1011        let mut line_end = layout.dimensions.columns();
1012
1013        if line == clamped_start_line {
1014            line_start = unclamped_start.column.0;
1015        }
1016        if line == clamped_end_line {
1017            line_end = unclamped_end.column.0 + 1; // +1 for inclusive
1018        }
1019
1020        highlighted_range_lines.push(HighlightedRangeLine {
1021            start_x: origin.x + line_start as f32 * layout.dimensions.cell_width,
1022            end_x: origin.x + line_end as f32 * layout.dimensions.cell_width,
1023        });
1024    }
1025
1026    Some((start_y, highlighted_range_lines))
1027}
1028
1029/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent.
1030fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
1031    let colors = theme.colors();
1032    match fg {
1033        // Named and theme defined colors
1034        terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n {
1035            NamedColor::Black => colors.terminal_ansi_black,
1036            NamedColor::Red => colors.terminal_ansi_red,
1037            NamedColor::Green => colors.terminal_ansi_green,
1038            NamedColor::Yellow => colors.terminal_ansi_yellow,
1039            NamedColor::Blue => colors.terminal_ansi_blue,
1040            NamedColor::Magenta => colors.terminal_ansi_magenta,
1041            NamedColor::Cyan => colors.terminal_ansi_cyan,
1042            NamedColor::White => colors.terminal_ansi_white,
1043            NamedColor::BrightBlack => colors.terminal_ansi_bright_black,
1044            NamedColor::BrightRed => colors.terminal_ansi_bright_red,
1045            NamedColor::BrightGreen => colors.terminal_ansi_bright_green,
1046            NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow,
1047            NamedColor::BrightBlue => colors.terminal_ansi_bright_blue,
1048            NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta,
1049            NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan,
1050            NamedColor::BrightWhite => colors.terminal_ansi_bright_white,
1051            NamedColor::Foreground => colors.text,
1052            NamedColor::Background => colors.background,
1053            NamedColor::Cursor => theme.players().local().cursor,
1054            NamedColor::DimBlack => colors.terminal_ansi_dim_black,
1055            NamedColor::DimRed => colors.terminal_ansi_dim_red,
1056            NamedColor::DimGreen => colors.terminal_ansi_dim_green,
1057            NamedColor::DimYellow => colors.terminal_ansi_dim_yellow,
1058            NamedColor::DimBlue => colors.terminal_ansi_dim_blue,
1059            NamedColor::DimMagenta => colors.terminal_ansi_dim_magenta,
1060            NamedColor::DimCyan => colors.terminal_ansi_dim_cyan,
1061            NamedColor::DimWhite => colors.terminal_ansi_dim_white,
1062            NamedColor::BrightForeground => colors.terminal_bright_foreground,
1063            NamedColor::DimForeground => colors.terminal_dim_foreground,
1064        },
1065        // 'True' colors
1066        terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => {
1067            terminal::rgba_color(rgb.r, rgb.g, rgb.b)
1068        }
1069        // 8 bit, indexed colors
1070        terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => {
1071            terminal::get_color_at_index(*i as usize, theme)
1072        }
1073    }
1074}