terminal_element.rs

   1use crate::color_contrast;
   2use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
   3use gpui::{
   4    AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase,
   5    Element, ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight,
   6    GlobalElementId, HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity,
   7    IntoElement, LayoutId, Length, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels,
   8    Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle,
   9    UTF16Selection, UnderlineStyle, WeakEntity, WhiteSpace, Window, div, fill, point, px, relative,
  10    size,
  11};
  12use itertools::Itertools;
  13use language::CursorShape;
  14use settings::Settings;
  15use std::time::Instant;
  16use terminal::{
  17    IndexedCell, Terminal, TerminalBounds, TerminalContent,
  18    alacritty_terminal::{
  19        grid::Dimensions,
  20        index::Point as AlacPoint,
  21        term::{TermMode, cell::Flags},
  22        vte::ansi::{
  23            Color::{self as AnsiColor, Named},
  24            CursorShape as AlacCursorShape, NamedColor,
  25        },
  26    },
  27    terminal_settings::TerminalSettings,
  28};
  29use theme::{ActiveTheme, Theme, ThemeSettings};
  30use ui::{ParentElement, Tooltip};
  31use util::ResultExt;
  32use workspace::Workspace;
  33
  34use std::mem;
  35use std::{fmt::Debug, ops::RangeInclusive, rc::Rc};
  36
  37use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalView};
  38
  39/// The information generated during layout that is necessary for painting.
  40pub struct LayoutState {
  41    hitbox: Hitbox,
  42    batched_text_runs: Vec<BatchedTextRun>,
  43    rects: Vec<LayoutRect>,
  44    relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
  45    cursor: Option<CursorLayout>,
  46    background_color: Hsla,
  47    dimensions: TerminalBounds,
  48    mode: TermMode,
  49    display_offset: usize,
  50    hyperlink_tooltip: Option<AnyElement>,
  51    gutter: Pixels,
  52    block_below_cursor_element: Option<AnyElement>,
  53    base_text_style: TextStyle,
  54    content_mode: ContentMode,
  55}
  56
  57/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
  58struct DisplayCursor {
  59    line: i32,
  60    col: usize,
  61}
  62
  63impl DisplayCursor {
  64    fn from(cursor_point: AlacPoint, display_offset: usize) -> Self {
  65        Self {
  66            line: cursor_point.line.0 + display_offset as i32,
  67            col: cursor_point.column.0,
  68        }
  69    }
  70
  71    pub fn line(&self) -> i32 {
  72        self.line
  73    }
  74
  75    pub fn col(&self) -> usize {
  76        self.col
  77    }
  78}
  79
  80/// A batched text run that combines multiple adjacent cells with the same style
  81#[derive(Debug)]
  82pub struct BatchedTextRun {
  83    pub start_point: AlacPoint<i32, i32>,
  84    pub text: String,
  85    pub cell_count: usize,
  86    pub style: TextRun,
  87    pub font_size: AbsoluteLength,
  88}
  89
  90impl BatchedTextRun {
  91    fn new_from_char(
  92        start_point: AlacPoint<i32, i32>,
  93        c: char,
  94        style: TextRun,
  95        font_size: AbsoluteLength,
  96    ) -> Self {
  97        let mut text = String::with_capacity(100); // Pre-allocate for typical line length
  98        text.push(c);
  99        BatchedTextRun {
 100            start_point,
 101            text,
 102            cell_count: 1,
 103            style,
 104            font_size,
 105        }
 106    }
 107
 108    fn can_append(&self, other_style: &TextRun) -> bool {
 109        self.style.font == other_style.font
 110            && self.style.color == other_style.color
 111            && self.style.background_color == other_style.background_color
 112            && self.style.underline == other_style.underline
 113            && self.style.strikethrough == other_style.strikethrough
 114    }
 115
 116    fn append_char(&mut self, c: char) {
 117        self.text.push(c);
 118        self.cell_count += 1;
 119        self.style.len += c.len_utf8();
 120    }
 121
 122    pub fn paint(
 123        &self,
 124        origin: Point<Pixels>,
 125        dimensions: &TerminalBounds,
 126        window: &mut Window,
 127        cx: &mut App,
 128    ) {
 129        let pos = Point::new(
 130            origin.x + self.start_point.column as f32 * dimensions.cell_width,
 131            origin.y + self.start_point.line as f32 * dimensions.line_height,
 132        );
 133
 134        let _ = window
 135            .text_system()
 136            .shape_line(
 137                self.text.clone().into(),
 138                self.font_size.to_pixels(window.rem_size()),
 139                &[self.style.clone()],
 140                Some(dimensions.cell_width),
 141            )
 142            .paint(pos, dimensions.line_height, window, cx);
 143    }
 144}
 145
 146#[derive(Clone, Debug, Default)]
 147pub struct LayoutRect {
 148    point: AlacPoint<i32, i32>,
 149    num_of_cells: usize,
 150    color: Hsla,
 151}
 152
 153impl LayoutRect {
 154    fn new(point: AlacPoint<i32, i32>, num_of_cells: usize, color: Hsla) -> LayoutRect {
 155        LayoutRect {
 156            point,
 157            num_of_cells,
 158            color,
 159        }
 160    }
 161
 162    pub fn paint(&self, origin: Point<Pixels>, dimensions: &TerminalBounds, window: &mut Window) {
 163        let position = {
 164            let alac_point = self.point;
 165            point(
 166                (origin.x + alac_point.column as f32 * dimensions.cell_width).floor(),
 167                origin.y + alac_point.line as f32 * dimensions.line_height,
 168            )
 169        };
 170        let size = point(
 171            (dimensions.cell_width * self.num_of_cells as f32).ceil(),
 172            dimensions.line_height,
 173        )
 174        .into();
 175
 176        window.paint_quad(fill(Bounds::new(position, size), self.color));
 177    }
 178}
 179
 180/// Represents a rectangular region with a specific background color
 181#[derive(Debug, Clone)]
 182struct BackgroundRegion {
 183    start_line: i32,
 184    start_col: i32,
 185    end_line: i32,
 186    end_col: i32,
 187    color: Hsla,
 188}
 189
 190impl BackgroundRegion {
 191    fn new(line: i32, col: i32, color: Hsla) -> Self {
 192        BackgroundRegion {
 193            start_line: line,
 194            start_col: col,
 195            end_line: line,
 196            end_col: col,
 197            color,
 198        }
 199    }
 200
 201    /// Check if this region can be merged with another region
 202    fn can_merge_with(&self, other: &BackgroundRegion) -> bool {
 203        if self.color != other.color {
 204            return false;
 205        }
 206
 207        // Check if regions are adjacent horizontally
 208        if self.start_line == other.start_line && self.end_line == other.end_line {
 209            return self.end_col + 1 == other.start_col || other.end_col + 1 == self.start_col;
 210        }
 211
 212        // Check if regions are adjacent vertically with same column span
 213        if self.start_col == other.start_col && self.end_col == other.end_col {
 214            return self.end_line + 1 == other.start_line || other.end_line + 1 == self.start_line;
 215        }
 216
 217        false
 218    }
 219
 220    /// Merge this region with another region
 221    fn merge_with(&mut self, other: &BackgroundRegion) {
 222        self.start_line = self.start_line.min(other.start_line);
 223        self.start_col = self.start_col.min(other.start_col);
 224        self.end_line = self.end_line.max(other.end_line);
 225        self.end_col = self.end_col.max(other.end_col);
 226    }
 227}
 228
 229/// Merge background regions to minimize the number of rectangles
 230fn merge_background_regions(regions: Vec<BackgroundRegion>) -> Vec<BackgroundRegion> {
 231    if regions.is_empty() {
 232        return regions;
 233    }
 234
 235    let mut merged = regions;
 236    let mut changed = true;
 237
 238    // Keep merging until no more merges are possible
 239    while changed {
 240        changed = false;
 241        let mut i = 0;
 242
 243        while i < merged.len() {
 244            let mut j = i + 1;
 245            while j < merged.len() {
 246                if merged[i].can_merge_with(&merged[j]) {
 247                    let other = merged.remove(j);
 248                    merged[i].merge_with(&other);
 249                    changed = true;
 250                } else {
 251                    j += 1;
 252                }
 253            }
 254            i += 1;
 255        }
 256    }
 257
 258    merged
 259}
 260
 261/// The GPUI element that paints the terminal.
 262/// We need to keep a reference to the model for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
 263pub struct TerminalElement {
 264    terminal: Entity<Terminal>,
 265    terminal_view: Entity<TerminalView>,
 266    workspace: WeakEntity<Workspace>,
 267    focus: FocusHandle,
 268    focused: bool,
 269    cursor_visible: bool,
 270    interactivity: Interactivity,
 271    mode: TerminalMode,
 272    block_below_cursor: Option<Rc<BlockProperties>>,
 273}
 274
 275impl InteractiveElement for TerminalElement {
 276    fn interactivity(&mut self) -> &mut Interactivity {
 277        &mut self.interactivity
 278    }
 279}
 280
 281impl StatefulInteractiveElement for TerminalElement {}
 282
 283impl TerminalElement {
 284    pub fn new(
 285        terminal: Entity<Terminal>,
 286        terminal_view: Entity<TerminalView>,
 287        workspace: WeakEntity<Workspace>,
 288        focus: FocusHandle,
 289        focused: bool,
 290        cursor_visible: bool,
 291        block_below_cursor: Option<Rc<BlockProperties>>,
 292        mode: TerminalMode,
 293    ) -> TerminalElement {
 294        TerminalElement {
 295            terminal,
 296            terminal_view,
 297            workspace,
 298            focused,
 299            focus: focus.clone(),
 300            cursor_visible,
 301            block_below_cursor,
 302            mode,
 303            interactivity: Default::default(),
 304        }
 305        .track_focus(&focus)
 306    }
 307
 308    //Vec<Range<AlacPoint>> -> Clip out the parts of the ranges
 309
 310    pub fn layout_grid(
 311        grid: impl Iterator<Item = IndexedCell>,
 312        start_line_offset: i32,
 313        text_style: &TextStyle,
 314        hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
 315        minimum_contrast: f32,
 316        cx: &App,
 317    ) -> (Vec<LayoutRect>, Vec<BatchedTextRun>) {
 318        let start_time = Instant::now();
 319        let theme = cx.theme();
 320
 321        // Pre-allocate with estimated capacity to reduce reallocations
 322        let estimated_cells = grid.size_hint().0;
 323        let estimated_runs = estimated_cells / 10; // Estimate ~10 cells per run
 324        let estimated_regions = estimated_cells / 20; // Estimate ~20 cells per background region
 325
 326        let mut batched_runs = Vec::with_capacity(estimated_runs);
 327        let mut cell_count = 0;
 328
 329        // Collect background regions for efficient merging
 330        let mut background_regions: Vec<BackgroundRegion> = Vec::with_capacity(estimated_regions);
 331        let mut current_batch: Option<BatchedTextRun> = None;
 332
 333        // First pass: collect all cells and their backgrounds
 334        let linegroups = grid.into_iter().chunk_by(|i| i.point.line);
 335        for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
 336            let alac_line = start_line_offset + line_index as i32;
 337
 338            // Flush any existing batch at line boundaries
 339            if let Some(batch) = current_batch.take() {
 340                batched_runs.push(batch);
 341            }
 342
 343            let mut previous_cell_had_extras = false;
 344
 345            for cell in line {
 346                let mut fg = cell.fg;
 347                let mut bg = cell.bg;
 348                if cell.flags.contains(Flags::INVERSE) {
 349                    mem::swap(&mut fg, &mut bg);
 350                }
 351
 352                // Collect background regions (skip default background)
 353                if !matches!(bg, Named(NamedColor::Background)) {
 354                    let color = convert_color(&bg, theme);
 355                    let col = cell.point.column.0 as i32;
 356
 357                    // Try to extend the last region if it's on the same line with the same color
 358                    if let Some(last_region) = background_regions.last_mut() {
 359                        if last_region.color == color
 360                            && last_region.start_line == alac_line
 361                            && last_region.end_line == alac_line
 362                            && last_region.end_col + 1 == col
 363                        {
 364                            last_region.end_col = col;
 365                        } else {
 366                            background_regions.push(BackgroundRegion::new(alac_line, col, color));
 367                        }
 368                    } else {
 369                        background_regions.push(BackgroundRegion::new(alac_line, col, color));
 370                    }
 371                }
 372                // Skip wide character spacers - they're just placeholders for the second cell of wide characters
 373                if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
 374                    continue;
 375                }
 376
 377                // Skip spaces that follow cells with extras (emoji variation sequences)
 378                if cell.c == ' ' && previous_cell_had_extras {
 379                    previous_cell_had_extras = false;
 380                    continue;
 381                }
 382                // Update tracking for next iteration
 383                previous_cell_had_extras = cell.extra.is_some();
 384
 385                //Layout current cell text
 386                {
 387                    if !is_blank(&cell) {
 388                        cell_count += 1;
 389                        let cell_style = TerminalElement::cell_style(
 390                            &cell,
 391                            fg,
 392                            bg,
 393                            theme,
 394                            text_style,
 395                            hyperlink,
 396                            minimum_contrast,
 397                        );
 398
 399                        let cell_point = AlacPoint::new(alac_line, cell.point.column.0 as i32);
 400
 401                        // Try to batch with existing run
 402                        if let Some(ref mut batch) = current_batch {
 403                            if batch.can_append(&cell_style)
 404                                && batch.start_point.line == cell_point.line
 405                                && batch.start_point.column + batch.cell_count as i32
 406                                    == cell_point.column
 407                            {
 408                                batch.append_char(cell.c);
 409                            } else {
 410                                // Flush current batch and start new one
 411                                let old_batch = current_batch.take().unwrap();
 412                                batched_runs.push(old_batch);
 413                                current_batch = Some(BatchedTextRun::new_from_char(
 414                                    cell_point,
 415                                    cell.c,
 416                                    cell_style,
 417                                    text_style.font_size,
 418                                ));
 419                            }
 420                        } else {
 421                            // Start new batch
 422                            current_batch = Some(BatchedTextRun::new_from_char(
 423                                cell_point,
 424                                cell.c,
 425                                cell_style,
 426                                text_style.font_size,
 427                            ));
 428                        }
 429                    };
 430                }
 431            }
 432        }
 433
 434        // Flush any remaining batch
 435        if let Some(batch) = current_batch {
 436            batched_runs.push(batch);
 437        }
 438
 439        // Second pass: merge background regions and convert to layout rects
 440        let region_count = background_regions.len();
 441        let merged_regions = merge_background_regions(background_regions);
 442        let mut rects = Vec::with_capacity(merged_regions.len() * 2); // Estimate 2 rects per merged region
 443
 444        // Convert merged regions to layout rects
 445        // Since LayoutRect only supports single-line rectangles, we need to split multi-line regions
 446        for region in merged_regions {
 447            for line in region.start_line..=region.end_line {
 448                rects.push(LayoutRect::new(
 449                    AlacPoint::new(line, region.start_col),
 450                    (region.end_col - region.start_col + 1) as usize,
 451                    region.color,
 452                ));
 453            }
 454        }
 455
 456        let layout_time = start_time.elapsed();
 457        log::debug!(
 458            "Terminal layout_grid: {} cells processed, {} batched runs created, {} rects (from {} merged regions), layout took {:?}",
 459            cell_count,
 460            batched_runs.len(),
 461            rects.len(),
 462            region_count,
 463            layout_time
 464        );
 465
 466        (rects, batched_runs)
 467    }
 468
 469    /// Computes the cursor position and expected block width, may return a zero width if x_for_index returns
 470    /// the same position for sequential indexes. Use em_width instead
 471    fn shape_cursor(
 472        cursor_point: DisplayCursor,
 473        size: TerminalBounds,
 474        text_fragment: &ShapedLine,
 475    ) -> Option<(Point<Pixels>, Pixels)> {
 476        if cursor_point.line() < size.total_lines() as i32 {
 477            let cursor_width = if text_fragment.width == Pixels::ZERO {
 478                size.cell_width()
 479            } else {
 480                text_fragment.width
 481            };
 482
 483            // Cursor should always surround as much of the text as possible,
 484            // hence when on pixel boundaries round the origin down and the width up
 485            Some((
 486                point(
 487                    (cursor_point.col() as f32 * size.cell_width()).floor(),
 488                    (cursor_point.line() as f32 * size.line_height()).floor(),
 489                ),
 490                cursor_width.ceil(),
 491            ))
 492        } else {
 493            None
 494        }
 495    }
 496
 497    /// Checks if a character is a decorative block/box-like character that should
 498    /// preserve its exact colors without contrast adjustment.
 499    ///
 500    /// Fixes https://github.com/zed-industries/zed/issues/34234 - we can
 501    /// expand this list if we run into more similar cases, but the goal
 502    /// is to be conservative here.
 503    fn is_decorative_character(ch: char) -> bool {
 504        matches!(
 505            ch as u32,
 506            // 0x2500..=0x257F Box Drawing
 507            // 0x2580..=0x259F Block Elements
 508            // 0x25A0..=0x25D7 Geometric Shapes (block/box-like subset)
 509            0x2500..=0x25D7
 510        )
 511    }
 512
 513    /// Converts the Alacritty cell styles to GPUI text styles and background color.
 514    fn cell_style(
 515        indexed: &IndexedCell,
 516        fg: terminal::alacritty_terminal::vte::ansi::Color,
 517        bg: terminal::alacritty_terminal::vte::ansi::Color,
 518        colors: &Theme,
 519        text_style: &TextStyle,
 520        hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
 521        minimum_contrast: f32,
 522    ) -> TextRun {
 523        let flags = indexed.cell.flags;
 524        let mut fg = convert_color(&fg, colors);
 525        let bg = convert_color(&bg, colors);
 526
 527        // Only apply contrast adjustment to non-decorative characters
 528        if !Self::is_decorative_character(indexed.c) {
 529            fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast);
 530        }
 531
 532        // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty
 533        // uses 0.75. We're using 0.7 because it's pretty well in the middle of that.
 534        if flags.intersects(Flags::DIM) {
 535            fg.a *= 0.7;
 536        }
 537
 538        let underline = (flags.intersects(Flags::ALL_UNDERLINES)
 539            || indexed.cell.hyperlink().is_some())
 540        .then(|| UnderlineStyle {
 541            color: Some(fg),
 542            thickness: Pixels::from(1.0),
 543            wavy: flags.contains(Flags::UNDERCURL),
 544        });
 545
 546        let strikethrough = flags
 547            .intersects(Flags::STRIKEOUT)
 548            .then(|| StrikethroughStyle {
 549                color: Some(fg),
 550                thickness: Pixels::from(1.0),
 551            });
 552
 553        let weight = if flags.intersects(Flags::BOLD) {
 554            FontWeight::BOLD
 555        } else {
 556            text_style.font_weight
 557        };
 558
 559        let style = if flags.intersects(Flags::ITALIC) {
 560            FontStyle::Italic
 561        } else {
 562            FontStyle::Normal
 563        };
 564
 565        let mut result = TextRun {
 566            len: indexed.c.len_utf8(),
 567            color: fg,
 568            background_color: None,
 569            font: Font {
 570                weight,
 571                style,
 572                ..text_style.font()
 573            },
 574            underline,
 575            strikethrough,
 576        };
 577
 578        if let Some((style, range)) = hyperlink {
 579            if range.contains(&indexed.point) {
 580                if let Some(underline) = style.underline {
 581                    result.underline = Some(underline);
 582                }
 583
 584                if let Some(color) = style.color {
 585                    result.color = color;
 586                }
 587            }
 588        }
 589
 590        result
 591    }
 592
 593    fn generic_button_handler<E>(
 594        connection: Entity<Terminal>,
 595        focus_handle: FocusHandle,
 596        steal_focus: bool,
 597        f: impl Fn(&mut Terminal, &E, &mut Context<Terminal>),
 598    ) -> impl Fn(&E, &mut Window, &mut App) {
 599        move |event, window, cx| {
 600            if steal_focus {
 601                window.focus(&focus_handle);
 602            } else if !focus_handle.is_focused(window) {
 603                return;
 604            }
 605            connection.update(cx, |terminal, cx| {
 606                f(terminal, event, cx);
 607
 608                cx.notify();
 609            })
 610        }
 611    }
 612
 613    fn register_mouse_listeners(
 614        &mut self,
 615        mode: TermMode,
 616        hitbox: &Hitbox,
 617        content_mode: &ContentMode,
 618        window: &mut Window,
 619    ) {
 620        let focus = self.focus.clone();
 621        let terminal = self.terminal.clone();
 622        let terminal_view = self.terminal_view.clone();
 623
 624        self.interactivity.on_mouse_down(MouseButton::Left, {
 625            let terminal = terminal.clone();
 626            let focus = focus.clone();
 627            let terminal_view = terminal_view.clone();
 628
 629            move |e, window, cx| {
 630                window.focus(&focus);
 631
 632                let scroll_top = terminal_view.read(cx).scroll_top;
 633                terminal.update(cx, |terminal, cx| {
 634                    let mut adjusted_event = e.clone();
 635                    if scroll_top > Pixels::ZERO {
 636                        adjusted_event.position.y += scroll_top;
 637                    }
 638                    terminal.mouse_down(&adjusted_event, cx);
 639                    cx.notify();
 640                })
 641            }
 642        });
 643
 644        window.on_mouse_event({
 645            let terminal = self.terminal.clone();
 646            let hitbox = hitbox.clone();
 647            let focus = focus.clone();
 648            let terminal_view = terminal_view.clone();
 649            move |e: &MouseMoveEvent, phase, window, cx| {
 650                if phase != DispatchPhase::Bubble {
 651                    return;
 652                }
 653
 654                if e.pressed_button.is_some() && !cx.has_active_drag() && focus.is_focused(window) {
 655                    let hovered = hitbox.is_hovered(window);
 656
 657                    let scroll_top = terminal_view.read(cx).scroll_top;
 658                    terminal.update(cx, |terminal, cx| {
 659                        if terminal.selection_started() || hovered {
 660                            let mut adjusted_event = e.clone();
 661                            if scroll_top > Pixels::ZERO {
 662                                adjusted_event.position.y += scroll_top;
 663                            }
 664                            terminal.mouse_drag(&adjusted_event, hitbox.bounds, cx);
 665                            cx.notify();
 666                        }
 667                    })
 668                }
 669
 670                if hitbox.is_hovered(window) {
 671                    terminal.update(cx, |terminal, cx| {
 672                        terminal.mouse_move(e, cx);
 673                    })
 674                }
 675            }
 676        });
 677
 678        self.interactivity.on_mouse_up(
 679            MouseButton::Left,
 680            TerminalElement::generic_button_handler(
 681                terminal.clone(),
 682                focus.clone(),
 683                false,
 684                move |terminal, e, cx| {
 685                    terminal.mouse_up(e, cx);
 686                },
 687            ),
 688        );
 689        self.interactivity.on_mouse_down(
 690            MouseButton::Middle,
 691            TerminalElement::generic_button_handler(
 692                terminal.clone(),
 693                focus.clone(),
 694                true,
 695                move |terminal, e, cx| {
 696                    terminal.mouse_down(e, cx);
 697                },
 698            ),
 699        );
 700
 701        if content_mode.is_scrollable() {
 702            self.interactivity.on_scroll_wheel({
 703                let terminal_view = self.terminal_view.downgrade();
 704                move |e, window, cx| {
 705                    terminal_view
 706                        .update(cx, |terminal_view, cx| {
 707                            if matches!(terminal_view.mode, TerminalMode::Standalone)
 708                                || terminal_view.focus_handle.is_focused(window)
 709                            {
 710                                terminal_view.scroll_wheel(e, cx);
 711                                cx.notify();
 712                            }
 713                        })
 714                        .ok();
 715                }
 716            });
 717        }
 718
 719        // Mouse mode handlers:
 720        // All mouse modes need the extra click handlers
 721        if mode.intersects(TermMode::MOUSE_MODE) {
 722            self.interactivity.on_mouse_down(
 723                MouseButton::Right,
 724                TerminalElement::generic_button_handler(
 725                    terminal.clone(),
 726                    focus.clone(),
 727                    true,
 728                    move |terminal, e, cx| {
 729                        terminal.mouse_down(e, cx);
 730                    },
 731                ),
 732            );
 733            self.interactivity.on_mouse_up(
 734                MouseButton::Right,
 735                TerminalElement::generic_button_handler(
 736                    terminal.clone(),
 737                    focus.clone(),
 738                    false,
 739                    move |terminal, e, cx| {
 740                        terminal.mouse_up(e, cx);
 741                    },
 742                ),
 743            );
 744            self.interactivity.on_mouse_up(
 745                MouseButton::Middle,
 746                TerminalElement::generic_button_handler(
 747                    terminal,
 748                    focus,
 749                    false,
 750                    move |terminal, e, cx| {
 751                        terminal.mouse_up(e, cx);
 752                    },
 753                ),
 754            );
 755        }
 756    }
 757
 758    fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
 759        let settings = ThemeSettings::get_global(cx).clone();
 760        let buffer_font_size = settings.buffer_font_size(cx);
 761        let rem_size_scale = {
 762            // Our default UI font size is 14px on a 16px base scale.
 763            // This means the default UI font size is 0.875rems.
 764            let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
 765
 766            // We then determine the delta between a single rem and the default font
 767            // size scale.
 768            let default_font_size_delta = 1. - default_font_size_scale;
 769
 770            // Finally, we add this delta to 1rem to get the scale factor that
 771            // should be used to scale up the UI.
 772            1. + default_font_size_delta
 773        };
 774
 775        Some(buffer_font_size * rem_size_scale)
 776    }
 777}
 778
 779impl Element for TerminalElement {
 780    type RequestLayoutState = ();
 781    type PrepaintState = LayoutState;
 782
 783    fn id(&self) -> Option<ElementId> {
 784        self.interactivity.element_id.clone()
 785    }
 786
 787    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
 788        None
 789    }
 790
 791    fn request_layout(
 792        &mut self,
 793        global_id: Option<&GlobalElementId>,
 794        inspector_id: Option<&gpui::InspectorElementId>,
 795        window: &mut Window,
 796        cx: &mut App,
 797    ) -> (LayoutId, Self::RequestLayoutState) {
 798        let height: Length = match self.terminal_view.read(cx).content_mode(window, cx) {
 799            ContentMode::Inline {
 800                displayed_lines,
 801                total_lines: _,
 802            } => {
 803                let rem_size = window.rem_size();
 804                let line_height = window.text_style().font_size.to_pixels(rem_size)
 805                    * TerminalSettings::get_global(cx)
 806                        .line_height
 807                        .value()
 808                        .to_pixels(rem_size)
 809                        .0;
 810                (displayed_lines * line_height).into()
 811            }
 812            ContentMode::Scrollable => {
 813                if let TerminalMode::Embedded { .. } = &self.mode {
 814                    let term = self.terminal.read(cx);
 815                    if !term.scrolled_to_top() && !term.scrolled_to_bottom() && self.focused {
 816                        self.interactivity.occlude_mouse();
 817                    }
 818                }
 819
 820                relative(1.).into()
 821            }
 822        };
 823
 824        let layout_id = self.interactivity.request_layout(
 825            global_id,
 826            inspector_id,
 827            window,
 828            cx,
 829            |mut style, window, cx| {
 830                style.size.width = relative(1.).into();
 831                style.size.height = height;
 832
 833                window.request_layout(style, None, cx)
 834            },
 835        );
 836        (layout_id, ())
 837    }
 838
 839    fn prepaint(
 840        &mut self,
 841        global_id: Option<&GlobalElementId>,
 842        inspector_id: Option<&gpui::InspectorElementId>,
 843        bounds: Bounds<Pixels>,
 844        _: &mut Self::RequestLayoutState,
 845        window: &mut Window,
 846        cx: &mut App,
 847    ) -> Self::PrepaintState {
 848        let rem_size = self.rem_size(cx);
 849        self.interactivity.prepaint(
 850            global_id,
 851            inspector_id,
 852            bounds,
 853            bounds.size,
 854            window,
 855            cx,
 856            |_, _, hitbox, window, cx| {
 857                let hitbox = hitbox.unwrap();
 858                let settings = ThemeSettings::get_global(cx).clone();
 859
 860                let buffer_font_size = settings.buffer_font_size(cx);
 861
 862                let terminal_settings = TerminalSettings::get_global(cx);
 863                let minimum_contrast = terminal_settings.minimum_contrast;
 864
 865                let font_family = terminal_settings.font_family.as_ref().map_or_else(
 866                    || settings.buffer_font.family.clone(),
 867                    |font_family| font_family.0.clone().into(),
 868                );
 869
 870                let font_fallbacks = terminal_settings
 871                    .font_fallbacks
 872                    .as_ref()
 873                    .or(settings.buffer_font.fallbacks.as_ref())
 874                    .cloned();
 875
 876                let font_features = terminal_settings
 877                    .font_features
 878                    .as_ref()
 879                    .unwrap_or(&FontFeatures::disable_ligatures())
 880                    .clone();
 881
 882                let font_weight = terminal_settings.font_weight.unwrap_or_default();
 883
 884                let line_height = terminal_settings.line_height.value();
 885
 886                let font_size = match &self.mode {
 887                    TerminalMode::Embedded { .. } => {
 888                        window.text_style().font_size.to_pixels(window.rem_size())
 889                    }
 890                    TerminalMode::Standalone => terminal_settings
 891                        .font_size
 892                        .map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)),
 893                };
 894
 895                let theme = cx.theme().clone();
 896
 897                let link_style = HighlightStyle {
 898                    color: Some(theme.colors().link_text_hover),
 899                    font_weight: Some(font_weight),
 900                    font_style: None,
 901                    background_color: None,
 902                    underline: Some(UnderlineStyle {
 903                        thickness: px(1.0),
 904                        color: Some(theme.colors().link_text_hover),
 905                        wavy: false,
 906                    }),
 907                    strikethrough: None,
 908                    fade_out: None,
 909                };
 910
 911                let text_style = TextStyle {
 912                    font_family,
 913                    font_features,
 914                    font_weight,
 915                    font_fallbacks,
 916                    font_size: font_size.into(),
 917                    font_style: FontStyle::Normal,
 918                    line_height: line_height.into(),
 919                    background_color: Some(theme.colors().terminal_ansi_background),
 920                    white_space: WhiteSpace::Normal,
 921                    // These are going to be overridden per-cell
 922                    color: theme.colors().terminal_foreground,
 923                    ..Default::default()
 924                };
 925
 926                let text_system = cx.text_system();
 927                let player_color = theme.players().local();
 928                let match_color = theme.colors().search_match_background;
 929                let gutter;
 930                let (dimensions, line_height_px) = {
 931                    let rem_size = window.rem_size();
 932                    let font_pixels = text_style.font_size.to_pixels(rem_size);
 933                    // TODO: line_height should be an f32 not an AbsoluteLength.
 934                    let line_height = font_pixels * line_height.to_pixels(rem_size).0;
 935                    let font_id = cx.text_system().resolve_font(&text_style.font());
 936
 937                    let cell_width = text_system
 938                        .advance(font_id, font_pixels, 'm')
 939                        .unwrap()
 940                        .width;
 941                    gutter = cell_width;
 942
 943                    let mut size = bounds.size;
 944                    size.width -= gutter;
 945
 946                    // https://github.com/zed-industries/zed/issues/2750
 947                    // if the terminal is one column wide, rendering 🦀
 948                    // causes alacritty to misbehave.
 949                    if size.width < cell_width * 2.0 {
 950                        size.width = cell_width * 2.0;
 951                    }
 952
 953                    let mut origin = bounds.origin;
 954                    origin.x += gutter;
 955
 956                    (
 957                        TerminalBounds::new(line_height, cell_width, Bounds { origin, size }),
 958                        line_height,
 959                    )
 960                };
 961
 962                let search_matches = self.terminal.read(cx).matches.clone();
 963
 964                let background_color = theme.colors().terminal_background;
 965
 966                let (last_hovered_word, hover_tooltip) =
 967                    self.terminal.update(cx, |terminal, cx| {
 968                        terminal.set_size(dimensions);
 969                        terminal.sync(window, cx);
 970
 971                        if window.modifiers().secondary()
 972                            && bounds.contains(&window.mouse_position())
 973                            && self.terminal_view.read(cx).hover.is_some()
 974                        {
 975                            let registered_hover = self.terminal_view.read(cx).hover.as_ref();
 976                            if terminal.last_content.last_hovered_word.as_ref()
 977                                == registered_hover.map(|hover| &hover.hovered_word)
 978                            {
 979                                (
 980                                    terminal.last_content.last_hovered_word.clone(),
 981                                    registered_hover.map(|hover| hover.tooltip.clone()),
 982                                )
 983                            } else {
 984                                (None, None)
 985                            }
 986                        } else {
 987                            (None, None)
 988                        }
 989                    });
 990
 991                let scroll_top = self.terminal_view.read(cx).scroll_top;
 992                let hyperlink_tooltip = hover_tooltip.map(|hover_tooltip| {
 993                    let offset = bounds.origin + point(gutter, px(0.)) - point(px(0.), scroll_top);
 994                    let mut element = div()
 995                        .size_full()
 996                        .id("terminal-element")
 997                        .tooltip(Tooltip::text(hover_tooltip))
 998                        .into_any_element();
 999                    element.prepaint_as_root(offset, bounds.size.into(), window, cx);
1000                    element
1001                });
1002
1003                let TerminalContent {
1004                    cells,
1005                    mode,
1006                    display_offset,
1007                    cursor_char,
1008                    selection,
1009                    cursor,
1010                    ..
1011                } = &self.terminal.read(cx).last_content;
1012                let mode = *mode;
1013                let display_offset = *display_offset;
1014
1015                // searches, highlights to a single range representations
1016                let mut relative_highlighted_ranges = Vec::new();
1017                for search_match in search_matches {
1018                    relative_highlighted_ranges.push((search_match, match_color))
1019                }
1020                if let Some(selection) = selection {
1021                    relative_highlighted_ranges
1022                        .push((selection.start..=selection.end, player_color.selection));
1023                }
1024
1025                // then have that representation be converted to the appropriate highlight data structure
1026
1027                let content_mode = self.terminal_view.read(cx).content_mode(window, cx);
1028                let (rects, batched_text_runs) = match content_mode {
1029                    ContentMode::Scrollable => {
1030                        // In scrollable mode, the terminal already provides cells
1031                        // that are correctly positioned for the current viewport
1032                        // based on its display_offset. We don't need additional filtering.
1033                        TerminalElement::layout_grid(
1034                            cells.iter().cloned(),
1035                            0,
1036                            &text_style,
1037                            last_hovered_word.as_ref().map(|last_hovered_word| {
1038                                (link_style, &last_hovered_word.word_match)
1039                            }),
1040                            minimum_contrast,
1041                            cx,
1042                        )
1043                    }
1044                    ContentMode::Inline { .. } => {
1045                        let intersection = window.content_mask().bounds.intersect(&bounds);
1046                        let start_row = (intersection.top() - bounds.top()) / line_height_px;
1047                        let end_row = start_row + intersection.size.height / line_height_px;
1048                        let line_range = (start_row as i32)..=(end_row as i32);
1049
1050                        TerminalElement::layout_grid(
1051                            cells
1052                                .iter()
1053                                .skip_while(|i| &i.point.line < line_range.start())
1054                                .take_while(|i| &i.point.line <= line_range.end())
1055                                .cloned(),
1056                            *line_range.start(),
1057                            &text_style,
1058                            last_hovered_word.as_ref().map(|last_hovered_word| {
1059                                (link_style, &last_hovered_word.word_match)
1060                            }),
1061                            minimum_contrast,
1062                            cx,
1063                        )
1064                    }
1065                };
1066
1067                // Layout cursor. Rectangle is used for IME, so we should lay it out even
1068                // if we don't end up showing it.
1069                let cursor = if let AlacCursorShape::Hidden = cursor.shape {
1070                    None
1071                } else {
1072                    let cursor_point = DisplayCursor::from(cursor.point, display_offset);
1073                    let cursor_text = {
1074                        let str_trxt = cursor_char.to_string();
1075                        let len = str_trxt.len();
1076                        window.text_system().shape_line(
1077                            str_trxt.into(),
1078                            text_style.font_size.to_pixels(window.rem_size()),
1079                            &[TextRun {
1080                                len,
1081                                font: text_style.font(),
1082                                color: theme.colors().terminal_ansi_background,
1083                                background_color: None,
1084                                underline: Default::default(),
1085                                strikethrough: None,
1086                            }],
1087                            None,
1088                        )
1089                    };
1090
1091                    let focused = self.focused;
1092                    TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
1093                        move |(cursor_position, block_width)| {
1094                            let (shape, text) = match cursor.shape {
1095                                AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
1096                                AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
1097                                AlacCursorShape::Underline => (CursorShape::Underline, None),
1098                                AlacCursorShape::Beam => (CursorShape::Bar, None),
1099                                AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
1100                                //This case is handled in the if wrapping the whole cursor layout
1101                                AlacCursorShape::Hidden => unreachable!(),
1102                            };
1103
1104                            CursorLayout::new(
1105                                cursor_position,
1106                                block_width,
1107                                dimensions.line_height,
1108                                theme.players().local().cursor,
1109                                shape,
1110                                text,
1111                            )
1112                        },
1113                    )
1114                };
1115
1116                let block_below_cursor_element = if let Some(block) = &self.block_below_cursor {
1117                    let terminal = self.terminal.read(cx);
1118                    if terminal.last_content.display_offset == 0 {
1119                        let target_line = terminal.last_content.cursor.point.line.0 + 1;
1120                        let render = &block.render;
1121                        let mut block_cx = BlockContext {
1122                            window,
1123                            context: cx,
1124                            dimensions,
1125                        };
1126                        let element = render(&mut block_cx);
1127                        let mut element = div().occlude().child(element).into_any_element();
1128                        let available_space = size(
1129                            AvailableSpace::Definite(dimensions.width() + gutter),
1130                            AvailableSpace::Definite(
1131                                block.height as f32 * dimensions.line_height(),
1132                            ),
1133                        );
1134                        let origin = bounds.origin
1135                            + point(px(0.), target_line as f32 * dimensions.line_height())
1136                            - point(px(0.), scroll_top);
1137                        window.with_rem_size(rem_size, |window| {
1138                            element.prepaint_as_root(origin, available_space, window, cx);
1139                        });
1140                        Some(element)
1141                    } else {
1142                        None
1143                    }
1144                } else {
1145                    None
1146                };
1147
1148                LayoutState {
1149                    hitbox,
1150                    batched_text_runs,
1151                    cursor,
1152                    background_color,
1153                    dimensions,
1154                    rects,
1155                    relative_highlighted_ranges,
1156                    mode,
1157                    display_offset,
1158                    hyperlink_tooltip,
1159                    gutter,
1160                    block_below_cursor_element,
1161                    base_text_style: text_style,
1162                    content_mode,
1163                }
1164            },
1165        )
1166    }
1167
1168    fn paint(
1169        &mut self,
1170        global_id: Option<&GlobalElementId>,
1171        inspector_id: Option<&gpui::InspectorElementId>,
1172        bounds: Bounds<Pixels>,
1173        _: &mut Self::RequestLayoutState,
1174        layout: &mut Self::PrepaintState,
1175        window: &mut Window,
1176        cx: &mut App,
1177    ) {
1178        let paint_start = Instant::now();
1179        window.with_content_mask(Some(ContentMask { bounds }), |window| {
1180            let scroll_top = self.terminal_view.read(cx).scroll_top;
1181
1182            window.paint_quad(fill(bounds, layout.background_color));
1183            let origin =
1184                bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top);
1185
1186            let marked_text_cloned: Option<String> = {
1187                let ime_state = self.terminal_view.read(cx);
1188                ime_state.marked_text.clone()
1189            };
1190
1191            let terminal_input_handler = TerminalInputHandler {
1192                terminal: self.terminal.clone(),
1193                terminal_view: self.terminal_view.clone(),
1194                cursor_bounds: layout
1195                    .cursor
1196                    .as_ref()
1197                    .map(|cursor| cursor.bounding_rect(origin)),
1198                workspace: self.workspace.clone(),
1199            };
1200
1201            self.register_mouse_listeners(
1202                layout.mode,
1203                &layout.hitbox,
1204                &layout.content_mode,
1205                window,
1206            );
1207            if window.modifiers().secondary()
1208                && bounds.contains(&window.mouse_position())
1209                && self.terminal_view.read(cx).hover.is_some()
1210            {
1211                window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
1212            } else {
1213                window.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
1214            }
1215
1216            let original_cursor = layout.cursor.take();
1217            let hyperlink_tooltip = layout.hyperlink_tooltip.take();
1218            let block_below_cursor_element = layout.block_below_cursor_element.take();
1219            self.interactivity.paint(
1220                global_id,
1221                inspector_id,
1222                bounds,
1223                Some(&layout.hitbox),
1224                window,
1225                cx,
1226                |_, window, cx| {
1227                    window.handle_input(&self.focus, terminal_input_handler, cx);
1228
1229                    window.on_key_event({
1230                        let this = self.terminal.clone();
1231                        move |event: &ModifiersChangedEvent, phase, window, cx| {
1232                            if phase != DispatchPhase::Bubble {
1233                                return;
1234                            }
1235
1236                            this.update(cx, |term, cx| {
1237                                term.try_modifiers_change(&event.modifiers, window, cx)
1238                            });
1239                        }
1240                    });
1241
1242                    for rect in &layout.rects {
1243                        rect.paint(origin, &layout.dimensions, window);
1244                    }
1245
1246                    for (relative_highlighted_range, color) in
1247                        layout.relative_highlighted_ranges.iter()
1248                    {
1249                        if let Some((start_y, highlighted_range_lines)) =
1250                            to_highlighted_range_lines(relative_highlighted_range, layout, origin)
1251                        {
1252                            let hr = HighlightedRange {
1253                                start_y,
1254                                line_height: layout.dimensions.line_height,
1255                                lines: highlighted_range_lines,
1256                                color: *color,
1257                                corner_radius: 0.15 * layout.dimensions.line_height,
1258                            };
1259                            hr.paint(true, bounds, window);
1260                        }
1261                    }
1262
1263                    // Paint batched text runs instead of individual cells
1264                    let text_paint_start = Instant::now();
1265                    for batch in &layout.batched_text_runs {
1266                        batch.paint(origin, &layout.dimensions, window, cx);
1267                    }
1268                    let text_paint_time = text_paint_start.elapsed();
1269
1270                    if let Some(text_to_mark) = &marked_text_cloned {
1271                        if !text_to_mark.is_empty() {
1272                            if let Some(cursor_layout) = &original_cursor {
1273                                let ime_position = cursor_layout.bounding_rect(origin).origin;
1274                                let mut ime_style = layout.base_text_style.clone();
1275                                ime_style.underline = Some(UnderlineStyle {
1276                                    color: Some(ime_style.color),
1277                                    thickness: px(1.0),
1278                                    wavy: false,
1279                                });
1280
1281                                let shaped_line = window.text_system().shape_line(
1282                                    text_to_mark.clone().into(),
1283                                    ime_style.font_size.to_pixels(window.rem_size()),
1284                                    &[TextRun {
1285                                        len: text_to_mark.len(),
1286                                        font: ime_style.font(),
1287                                        color: ime_style.color,
1288                                        background_color: None,
1289                                        underline: ime_style.underline,
1290                                        strikethrough: None,
1291                                    }],
1292                                    None
1293                                );
1294                                shaped_line
1295                                    .paint(ime_position, layout.dimensions.line_height, window, cx)
1296                                    .log_err();
1297                            }
1298                        }
1299                    }
1300
1301                    if self.cursor_visible && marked_text_cloned.is_none() {
1302                        if let Some(mut cursor) = original_cursor {
1303                            cursor.paint(origin, window, cx);
1304                        }
1305                    }
1306
1307                    if let Some(mut element) = block_below_cursor_element {
1308                        element.paint(window, cx);
1309                    }
1310
1311                    if let Some(mut element) = hyperlink_tooltip {
1312                        element.paint(window, cx);
1313                    }
1314                    let total_paint_time = paint_start.elapsed();
1315                    log::debug!(
1316                        "Terminal paint: {} text runs, {} rects, text paint took {:?}, total paint took {:?}",
1317                        layout.batched_text_runs.len(),
1318                        layout.rects.len(),
1319                        text_paint_time,
1320                        total_paint_time
1321                    );
1322                },
1323            );
1324        });
1325    }
1326}
1327
1328impl IntoElement for TerminalElement {
1329    type Element = Self;
1330
1331    fn into_element(self) -> Self::Element {
1332        self
1333    }
1334}
1335
1336struct TerminalInputHandler {
1337    terminal: Entity<Terminal>,
1338    terminal_view: Entity<TerminalView>,
1339    workspace: WeakEntity<Workspace>,
1340    cursor_bounds: Option<Bounds<Pixels>>,
1341}
1342
1343impl InputHandler for TerminalInputHandler {
1344    fn selected_text_range(
1345        &mut self,
1346        _ignore_disabled_input: bool,
1347        _: &mut Window,
1348        cx: &mut App,
1349    ) -> Option<UTF16Selection> {
1350        if self
1351            .terminal
1352            .read(cx)
1353            .last_content
1354            .mode
1355            .contains(TermMode::ALT_SCREEN)
1356        {
1357            None
1358        } else {
1359            Some(UTF16Selection {
1360                range: 0..0,
1361                reversed: false,
1362            })
1363        }
1364    }
1365
1366    fn marked_text_range(
1367        &mut self,
1368        _window: &mut Window,
1369        cx: &mut App,
1370    ) -> Option<std::ops::Range<usize>> {
1371        self.terminal_view.read(cx).marked_text_range()
1372    }
1373
1374    fn text_for_range(
1375        &mut self,
1376        _: std::ops::Range<usize>,
1377        _: &mut Option<std::ops::Range<usize>>,
1378        _: &mut Window,
1379        _: &mut App,
1380    ) -> Option<String> {
1381        None
1382    }
1383
1384    fn replace_text_in_range(
1385        &mut self,
1386        _replacement_range: Option<std::ops::Range<usize>>,
1387        text: &str,
1388        window: &mut Window,
1389        cx: &mut App,
1390    ) {
1391        self.terminal_view.update(cx, |view, view_cx| {
1392            view.clear_marked_text(view_cx);
1393            view.commit_text(text, view_cx);
1394        });
1395
1396        self.workspace
1397            .update(cx, |this, cx| {
1398                window.invalidate_character_coordinates();
1399                let project = this.project().read(cx);
1400                let telemetry = project.client().telemetry().clone();
1401                telemetry.log_edit_event("terminal", project.is_via_ssh());
1402            })
1403            .ok();
1404    }
1405
1406    fn replace_and_mark_text_in_range(
1407        &mut self,
1408        _range_utf16: Option<std::ops::Range<usize>>,
1409        new_text: &str,
1410        new_marked_range: Option<std::ops::Range<usize>>,
1411        _window: &mut Window,
1412        cx: &mut App,
1413    ) {
1414        if let Some(range) = new_marked_range {
1415            self.terminal_view.update(cx, |view, view_cx| {
1416                view.set_marked_text(new_text.to_string(), range, view_cx);
1417            });
1418        }
1419    }
1420
1421    fn unmark_text(&mut self, _window: &mut Window, cx: &mut App) {
1422        self.terminal_view.update(cx, |view, view_cx| {
1423            view.clear_marked_text(view_cx);
1424        });
1425    }
1426
1427    fn bounds_for_range(
1428        &mut self,
1429        range_utf16: std::ops::Range<usize>,
1430        _window: &mut Window,
1431        cx: &mut App,
1432    ) -> Option<Bounds<Pixels>> {
1433        let term_bounds = self.terminal_view.read(cx).terminal_bounds(cx);
1434
1435        let mut bounds = self.cursor_bounds?;
1436        let offset_x = term_bounds.cell_width * range_utf16.start as f32;
1437        bounds.origin.x += offset_x;
1438
1439        Some(bounds)
1440    }
1441
1442    fn apple_press_and_hold_enabled(&mut self) -> bool {
1443        false
1444    }
1445
1446    fn character_index_for_point(
1447        &mut self,
1448        _point: Point<Pixels>,
1449        _window: &mut Window,
1450        _cx: &mut App,
1451    ) -> Option<usize> {
1452        None
1453    }
1454}
1455
1456pub fn is_blank(cell: &IndexedCell) -> bool {
1457    if cell.c != ' ' {
1458        return false;
1459    }
1460
1461    if cell.bg != AnsiColor::Named(NamedColor::Background) {
1462        return false;
1463    }
1464
1465    if cell.hyperlink().is_some() {
1466        return false;
1467    }
1468
1469    if cell
1470        .flags
1471        .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
1472    {
1473        return false;
1474    }
1475
1476    return true;
1477}
1478
1479fn to_highlighted_range_lines(
1480    range: &RangeInclusive<AlacPoint>,
1481    layout: &LayoutState,
1482    origin: Point<Pixels>,
1483) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
1484    // Step 1. Normalize the points to be viewport relative.
1485    // When display_offset = 1, here's how the grid is arranged:
1486    //-2,0 -2,1...
1487    //--- Viewport top
1488    //-1,0 -1,1...
1489    //--------- Terminal Top
1490    // 0,0  0,1...
1491    // 1,0  1,1...
1492    //--- Viewport Bottom
1493    // 2,0  2,1...
1494    //--------- Terminal Bottom
1495
1496    // Normalize to viewport relative, from terminal relative.
1497    // lines are i32s, which are negative above the top left corner of the terminal
1498    // If the user has scrolled, we use the display_offset to tell us which offset
1499    // of the grid data we should be looking at. But for the rendering step, we don't
1500    // want negatives. We want things relative to the 'viewport' (the area of the grid
1501    // which is currently shown according to the display offset)
1502    let unclamped_start = AlacPoint::new(
1503        range.start().line + layout.display_offset,
1504        range.start().column,
1505    );
1506    let unclamped_end =
1507        AlacPoint::new(range.end().line + layout.display_offset, range.end().column);
1508
1509    // Step 2. Clamp range to viewport, and return None if it doesn't overlap
1510    if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 {
1511        return None;
1512    }
1513
1514    let clamped_start_line = unclamped_start.line.0.max(0) as usize;
1515    let clamped_end_line = unclamped_end
1516        .line
1517        .0
1518        .min(layout.dimensions.num_lines() as i32) as usize;
1519    //Convert the start of the range to pixels
1520    let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height;
1521
1522    // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
1523    //  (also convert to pixels)
1524    let mut highlighted_range_lines = Vec::new();
1525    for line in clamped_start_line..=clamped_end_line {
1526        let mut line_start = 0;
1527        let mut line_end = layout.dimensions.columns();
1528
1529        if line == clamped_start_line {
1530            line_start = unclamped_start.column.0;
1531        }
1532        if line == clamped_end_line {
1533            line_end = unclamped_end.column.0 + 1; // +1 for inclusive
1534        }
1535
1536        highlighted_range_lines.push(HighlightedRangeLine {
1537            start_x: origin.x + line_start as f32 * layout.dimensions.cell_width,
1538            end_x: origin.x + line_end as f32 * layout.dimensions.cell_width,
1539        });
1540    }
1541
1542    Some((start_y, highlighted_range_lines))
1543}
1544
1545/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent.
1546pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
1547    let colors = theme.colors();
1548    match fg {
1549        // Named and theme defined colors
1550        terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n {
1551            NamedColor::Black => colors.terminal_ansi_black,
1552            NamedColor::Red => colors.terminal_ansi_red,
1553            NamedColor::Green => colors.terminal_ansi_green,
1554            NamedColor::Yellow => colors.terminal_ansi_yellow,
1555            NamedColor::Blue => colors.terminal_ansi_blue,
1556            NamedColor::Magenta => colors.terminal_ansi_magenta,
1557            NamedColor::Cyan => colors.terminal_ansi_cyan,
1558            NamedColor::White => colors.terminal_ansi_white,
1559            NamedColor::BrightBlack => colors.terminal_ansi_bright_black,
1560            NamedColor::BrightRed => colors.terminal_ansi_bright_red,
1561            NamedColor::BrightGreen => colors.terminal_ansi_bright_green,
1562            NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow,
1563            NamedColor::BrightBlue => colors.terminal_ansi_bright_blue,
1564            NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta,
1565            NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan,
1566            NamedColor::BrightWhite => colors.terminal_ansi_bright_white,
1567            NamedColor::Foreground => colors.terminal_foreground,
1568            NamedColor::Background => colors.terminal_ansi_background,
1569            NamedColor::Cursor => theme.players().local().cursor,
1570            NamedColor::DimBlack => colors.terminal_ansi_dim_black,
1571            NamedColor::DimRed => colors.terminal_ansi_dim_red,
1572            NamedColor::DimGreen => colors.terminal_ansi_dim_green,
1573            NamedColor::DimYellow => colors.terminal_ansi_dim_yellow,
1574            NamedColor::DimBlue => colors.terminal_ansi_dim_blue,
1575            NamedColor::DimMagenta => colors.terminal_ansi_dim_magenta,
1576            NamedColor::DimCyan => colors.terminal_ansi_dim_cyan,
1577            NamedColor::DimWhite => colors.terminal_ansi_dim_white,
1578            NamedColor::BrightForeground => colors.terminal_bright_foreground,
1579            NamedColor::DimForeground => colors.terminal_dim_foreground,
1580        },
1581        // 'True' colors
1582        terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => {
1583            terminal::rgba_color(rgb.r, rgb.g, rgb.b)
1584        }
1585        // 8 bit, indexed colors
1586        terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => {
1587            terminal::get_color_at_index(*i as usize, theme)
1588        }
1589    }
1590}
1591
1592#[cfg(test)]
1593mod tests {
1594    use super::*;
1595    use gpui::{AbsoluteLength, Hsla, font};
1596
1597    #[test]
1598    fn test_is_decorative_character() {
1599        // Box Drawing characters (U+2500 to U+257F)
1600        assert!(TerminalElement::is_decorative_character('─')); // U+2500
1601        assert!(TerminalElement::is_decorative_character('│')); // U+2502
1602        assert!(TerminalElement::is_decorative_character('┌')); // U+250C
1603        assert!(TerminalElement::is_decorative_character('┐')); // U+2510
1604        assert!(TerminalElement::is_decorative_character('└')); // U+2514
1605        assert!(TerminalElement::is_decorative_character('┘')); // U+2518
1606        assert!(TerminalElement::is_decorative_character('┼')); // U+253C
1607
1608        // Block Elements (U+2580 to U+259F)
1609        assert!(TerminalElement::is_decorative_character('▀')); // U+2580
1610        assert!(TerminalElement::is_decorative_character('▄')); // U+2584
1611        assert!(TerminalElement::is_decorative_character('█')); // U+2588
1612        assert!(TerminalElement::is_decorative_character('░')); // U+2591
1613        assert!(TerminalElement::is_decorative_character('▒')); // U+2592
1614        assert!(TerminalElement::is_decorative_character('▓')); // U+2593
1615
1616        // Geometric Shapes - block/box-like subset (U+25A0 to U+25D7)
1617        assert!(TerminalElement::is_decorative_character('■')); // U+25A0
1618        assert!(TerminalElement::is_decorative_character('□')); // U+25A1
1619        assert!(TerminalElement::is_decorative_character('▲')); // U+25B2
1620        assert!(TerminalElement::is_decorative_character('▼')); // U+25BC
1621        assert!(TerminalElement::is_decorative_character('◆')); // U+25C6
1622        assert!(TerminalElement::is_decorative_character('●')); // U+25CF
1623
1624        // The specific character from the issue
1625        assert!(TerminalElement::is_decorative_character('◗')); // U+25D7
1626
1627        // Characters that should NOT be considered decorative
1628        assert!(!TerminalElement::is_decorative_character('A'));
1629        assert!(!TerminalElement::is_decorative_character('a'));
1630        assert!(!TerminalElement::is_decorative_character('0'));
1631        assert!(!TerminalElement::is_decorative_character(' '));
1632        assert!(!TerminalElement::is_decorative_character('←')); // U+2190 (Arrow, not in our ranges)
1633        assert!(!TerminalElement::is_decorative_character('→')); // U+2192 (Arrow, not in our ranges)
1634        assert!(!TerminalElement::is_decorative_character('◘')); // U+25D8 (Just outside our range)
1635        assert!(!TerminalElement::is_decorative_character('◙')); // U+25D9 (Just outside our range)
1636    }
1637
1638    #[test]
1639    fn test_decorative_character_boundary_cases() {
1640        // Test exact boundaries of our ranges
1641        // Box Drawing range boundaries
1642        assert!(TerminalElement::is_decorative_character('\u{2500}')); // First char
1643        assert!(TerminalElement::is_decorative_character('\u{257F}')); // Last char
1644        assert!(!TerminalElement::is_decorative_character('\u{24FF}')); // Just before
1645
1646        // Block Elements range boundaries
1647        assert!(TerminalElement::is_decorative_character('\u{2580}')); // First char
1648        assert!(TerminalElement::is_decorative_character('\u{259F}')); // Last char
1649
1650        // Geometric Shapes subset boundaries
1651        assert!(TerminalElement::is_decorative_character('\u{25A0}')); // First char
1652        assert!(TerminalElement::is_decorative_character('\u{25D7}')); // Last char (◗)
1653        assert!(!TerminalElement::is_decorative_character('\u{25D8}')); // Just after
1654    }
1655
1656    #[test]
1657    fn test_decorative_characters_bypass_contrast_adjustment() {
1658        // Decorative characters should not be affected by contrast adjustment
1659
1660        // The specific character from issue #34234
1661        let problematic_char = '◗'; // U+25D7
1662        assert!(
1663            TerminalElement::is_decorative_character(problematic_char),
1664            "Character ◗ (U+25D7) should be recognized as decorative"
1665        );
1666
1667        // Verify some other commonly used decorative characters
1668        assert!(TerminalElement::is_decorative_character('│')); // Vertical line
1669        assert!(TerminalElement::is_decorative_character('─')); // Horizontal line
1670        assert!(TerminalElement::is_decorative_character('█')); // Full block
1671        assert!(TerminalElement::is_decorative_character('▓')); // Dark shade
1672        assert!(TerminalElement::is_decorative_character('■')); // Black square
1673        assert!(TerminalElement::is_decorative_character('●')); // Black circle
1674
1675        // Verify normal text characters are NOT decorative
1676        assert!(!TerminalElement::is_decorative_character('A'));
1677        assert!(!TerminalElement::is_decorative_character('1'));
1678        assert!(!TerminalElement::is_decorative_character('$'));
1679        assert!(!TerminalElement::is_decorative_character(' '));
1680    }
1681
1682    #[test]
1683    fn test_contrast_adjustment_logic() {
1684        // Test the core contrast adjustment logic without needing full app context
1685
1686        // Test case 1: Light colors (poor contrast)
1687        let white_fg = gpui::Hsla {
1688            h: 0.0,
1689            s: 0.0,
1690            l: 1.0,
1691            a: 1.0,
1692        };
1693        let light_gray_bg = gpui::Hsla {
1694            h: 0.0,
1695            s: 0.0,
1696            l: 0.95,
1697            a: 1.0,
1698        };
1699
1700        // Should have poor contrast
1701        let actual_contrast = color_contrast::apca_contrast(white_fg, light_gray_bg).abs();
1702        assert!(
1703            actual_contrast < 30.0,
1704            "White on light gray should have poor APCA contrast: {}",
1705            actual_contrast
1706        );
1707
1708        // After adjustment with minimum APCA contrast of 45, should be darker
1709        let adjusted = color_contrast::ensure_minimum_contrast(white_fg, light_gray_bg, 45.0);
1710        assert!(
1711            adjusted.l < white_fg.l,
1712            "Adjusted color should be darker than original"
1713        );
1714        let adjusted_contrast = color_contrast::apca_contrast(adjusted, light_gray_bg).abs();
1715        assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast");
1716
1717        // Test case 2: Dark colors (poor contrast)
1718        let black_fg = gpui::Hsla {
1719            h: 0.0,
1720            s: 0.0,
1721            l: 0.0,
1722            a: 1.0,
1723        };
1724        let dark_gray_bg = gpui::Hsla {
1725            h: 0.0,
1726            s: 0.0,
1727            l: 0.05,
1728            a: 1.0,
1729        };
1730
1731        // Should have poor contrast
1732        let actual_contrast = color_contrast::apca_contrast(black_fg, dark_gray_bg).abs();
1733        assert!(
1734            actual_contrast < 30.0,
1735            "Black on dark gray should have poor APCA contrast: {}",
1736            actual_contrast
1737        );
1738
1739        // After adjustment with minimum APCA contrast of 45, should be lighter
1740        let adjusted = color_contrast::ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0);
1741        assert!(
1742            adjusted.l > black_fg.l,
1743            "Adjusted color should be lighter than original"
1744        );
1745        let adjusted_contrast = color_contrast::apca_contrast(adjusted, dark_gray_bg).abs();
1746        assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast");
1747
1748        // Test case 3: Already good contrast
1749        let good_contrast = color_contrast::ensure_minimum_contrast(black_fg, white_fg, 45.0);
1750        assert_eq!(
1751            good_contrast, black_fg,
1752            "Good contrast should not be adjusted"
1753        );
1754    }
1755
1756    #[test]
1757    fn test_white_on_white_contrast_issue() {
1758        // This test reproduces the exact issue from the bug report
1759        // where white ANSI text on white background should be adjusted
1760
1761        // Simulate One Light theme colors
1762        let white_fg = gpui::Hsla {
1763            h: 0.0,
1764            s: 0.0,
1765            l: 0.98, // #fafafaff is approximately 98% lightness
1766            a: 1.0,
1767        };
1768        let white_bg = gpui::Hsla {
1769            h: 0.0,
1770            s: 0.0,
1771            l: 0.98, // Same as foreground - this is the problem!
1772            a: 1.0,
1773        };
1774
1775        // With minimum contrast of 0.0, no adjustment should happen
1776        let no_adjust = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 0.0);
1777        assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0");
1778
1779        // With minimum APCA contrast of 15, it should adjust to a darker color
1780        let adjusted = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 15.0);
1781        assert!(
1782            adjusted.l < white_fg.l,
1783            "White on white should become darker, got l={}",
1784            adjusted.l
1785        );
1786
1787        // Verify the contrast is now acceptable
1788        let new_contrast = color_contrast::apca_contrast(adjusted, white_bg).abs();
1789        assert!(
1790            new_contrast >= 15.0,
1791            "Adjusted APCA contrast {} should be >= 15.0",
1792            new_contrast
1793        );
1794    }
1795
1796    #[test]
1797    fn test_batched_text_run_can_append() {
1798        let style1 = TextRun {
1799            len: 1,
1800            font: font("Helvetica"),
1801            color: Hsla::red(),
1802            background_color: None,
1803            underline: None,
1804            strikethrough: None,
1805        };
1806
1807        let style2 = TextRun {
1808            len: 1,
1809            font: font("Helvetica"),
1810            color: Hsla::red(),
1811            background_color: None,
1812            underline: None,
1813            strikethrough: None,
1814        };
1815
1816        let style3 = TextRun {
1817            len: 1,
1818            font: font("Helvetica"),
1819            color: Hsla::blue(), // Different color
1820            background_color: None,
1821            underline: None,
1822            strikethrough: None,
1823        };
1824
1825        let font_size = AbsoluteLength::Pixels(px(12.0));
1826        let batch =
1827            BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1.clone(), font_size);
1828
1829        // Should be able to append same style
1830        assert!(batch.can_append(&style2));
1831
1832        // Should not be able to append different style
1833        assert!(!batch.can_append(&style3));
1834    }
1835
1836    #[test]
1837    fn test_batched_text_run_append() {
1838        let style = TextRun {
1839            len: 1,
1840            font: font("Helvetica"),
1841            color: Hsla::red(),
1842            background_color: None,
1843            underline: None,
1844            strikethrough: None,
1845        };
1846
1847        let font_size = AbsoluteLength::Pixels(px(12.0));
1848        let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style, font_size);
1849
1850        assert_eq!(batch.text, "a");
1851        assert_eq!(batch.cell_count, 1);
1852        assert_eq!(batch.style.len, 1);
1853
1854        batch.append_char('b');
1855
1856        assert_eq!(batch.text, "ab");
1857        assert_eq!(batch.cell_count, 2);
1858        assert_eq!(batch.style.len, 2);
1859
1860        batch.append_char('c');
1861
1862        assert_eq!(batch.text, "abc");
1863        assert_eq!(batch.cell_count, 3);
1864        assert_eq!(batch.style.len, 3);
1865    }
1866
1867    #[test]
1868    fn test_batched_text_run_append_char() {
1869        let style = TextRun {
1870            len: 1,
1871            font: font("Helvetica"),
1872            color: Hsla::red(),
1873            background_color: None,
1874            underline: None,
1875            strikethrough: None,
1876        };
1877
1878        let font_size = AbsoluteLength::Pixels(px(12.0));
1879        let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size);
1880
1881        assert_eq!(batch.text, "x");
1882        assert_eq!(batch.cell_count, 1);
1883        assert_eq!(batch.style.len, 1);
1884
1885        batch.append_char('y');
1886
1887        assert_eq!(batch.text, "xy");
1888        assert_eq!(batch.cell_count, 2);
1889        assert_eq!(batch.style.len, 2);
1890
1891        // Test with multi-byte character
1892        batch.append_char('😀');
1893
1894        assert_eq!(batch.text, "xy😀");
1895        assert_eq!(batch.cell_count, 3);
1896        assert_eq!(batch.style.len, 6); // 1 + 1 + 4 bytes for emoji
1897    }
1898
1899    #[test]
1900    fn test_background_region_can_merge() {
1901        let color1 = Hsla::red();
1902        let color2 = Hsla::blue();
1903
1904        // Test horizontal merging
1905        let mut region1 = BackgroundRegion::new(0, 0, color1);
1906        region1.end_col = 5;
1907        let region2 = BackgroundRegion::new(0, 6, color1);
1908        assert!(region1.can_merge_with(&region2));
1909
1910        // Test vertical merging with same column span
1911        let mut region3 = BackgroundRegion::new(0, 0, color1);
1912        region3.end_col = 5;
1913        let mut region4 = BackgroundRegion::new(1, 0, color1);
1914        region4.end_col = 5;
1915        assert!(region3.can_merge_with(&region4));
1916
1917        // Test cannot merge different colors
1918        let region5 = BackgroundRegion::new(0, 0, color1);
1919        let region6 = BackgroundRegion::new(0, 1, color2);
1920        assert!(!region5.can_merge_with(&region6));
1921
1922        // Test cannot merge non-adjacent regions
1923        let region7 = BackgroundRegion::new(0, 0, color1);
1924        let region8 = BackgroundRegion::new(0, 2, color1);
1925        assert!(!region7.can_merge_with(&region8));
1926
1927        // Test cannot merge vertical regions with different column spans
1928        let mut region9 = BackgroundRegion::new(0, 0, color1);
1929        region9.end_col = 5;
1930        let mut region10 = BackgroundRegion::new(1, 0, color1);
1931        region10.end_col = 6;
1932        assert!(!region9.can_merge_with(&region10));
1933    }
1934
1935    #[test]
1936    fn test_background_region_merge() {
1937        let color = Hsla::red();
1938
1939        // Test horizontal merge
1940        let mut region1 = BackgroundRegion::new(0, 0, color);
1941        region1.end_col = 5;
1942        let mut region2 = BackgroundRegion::new(0, 6, color);
1943        region2.end_col = 10;
1944        region1.merge_with(&region2);
1945        assert_eq!(region1.start_col, 0);
1946        assert_eq!(region1.end_col, 10);
1947        assert_eq!(region1.start_line, 0);
1948        assert_eq!(region1.end_line, 0);
1949
1950        // Test vertical merge
1951        let mut region3 = BackgroundRegion::new(0, 0, color);
1952        region3.end_col = 5;
1953        let mut region4 = BackgroundRegion::new(1, 0, color);
1954        region4.end_col = 5;
1955        region3.merge_with(&region4);
1956        assert_eq!(region3.start_col, 0);
1957        assert_eq!(region3.end_col, 5);
1958        assert_eq!(region3.start_line, 0);
1959        assert_eq!(region3.end_line, 1);
1960    }
1961
1962    #[test]
1963    fn test_merge_background_regions() {
1964        let color = Hsla::red();
1965
1966        // Test merging multiple adjacent regions
1967        let regions = vec![
1968            BackgroundRegion::new(0, 0, color),
1969            BackgroundRegion::new(0, 1, color),
1970            BackgroundRegion::new(0, 2, color),
1971            BackgroundRegion::new(1, 0, color),
1972            BackgroundRegion::new(1, 1, color),
1973            BackgroundRegion::new(1, 2, color),
1974        ];
1975
1976        let merged = merge_background_regions(regions);
1977        assert_eq!(merged.len(), 1);
1978        assert_eq!(merged[0].start_line, 0);
1979        assert_eq!(merged[0].end_line, 1);
1980        assert_eq!(merged[0].start_col, 0);
1981        assert_eq!(merged[0].end_col, 2);
1982
1983        // Test with non-mergeable regions
1984        let color2 = Hsla::blue();
1985        let regions2 = vec![
1986            BackgroundRegion::new(0, 0, color),
1987            BackgroundRegion::new(0, 2, color),  // Gap at column 1
1988            BackgroundRegion::new(1, 0, color2), // Different color
1989        ];
1990
1991        let merged2 = merge_background_regions(regions2);
1992        assert_eq!(merged2.len(), 3);
1993    }
1994}