terminal_element.rs

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