terminal_element.rs

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