connected_el.rs

   1use alacritty_terminal::{
   2    ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
   3    grid::{Dimensions, Scroll},
   4    index::{Column as GridCol, Direction, Line as GridLine, Point, Side},
   5    selection::SelectionRange,
   6    term::{
   7        cell::{Cell, Flags},
   8        TermMode,
   9    },
  10};
  11use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
  12use gpui::{
  13    color::Color,
  14    elements::*,
  15    fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
  16    geometry::{
  17        rect::RectF,
  18        vector::{vec2f, Vector2F},
  19    },
  20    json::json,
  21    text_layout::{Line, RunStyle},
  22    Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseButtonEvent,
  23    MouseRegion, PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle,
  24    WeakViewHandle,
  25};
  26use itertools::Itertools;
  27use ordered_float::OrderedFloat;
  28use settings::Settings;
  29use theme::TerminalStyle;
  30use util::ResultExt;
  31
  32use std::{
  33    cmp::min,
  34    mem,
  35    ops::{Deref, Range},
  36};
  37use std::{fmt::Debug, ops::Sub};
  38
  39use crate::{
  40    connected_view::{ConnectedView, DeployContextMenu},
  41    mappings::colors::convert_color,
  42    Terminal, TerminalSize,
  43};
  44
  45///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
  46///Scroll multiplier that is set to 3 by default. This will be removed when I
  47///Implement scroll bars.
  48pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
  49
  50///The information generated during layout that is nescessary for painting
  51pub struct LayoutState {
  52    cells: Vec<LayoutCell>,
  53    rects: Vec<LayoutRect>,
  54    highlights: Vec<RelativeHighlightedRange>,
  55    cursor: Option<Cursor>,
  56    background_color: Color,
  57    selection_color: Color,
  58    size: TerminalSize,
  59    display_offset: usize,
  60}
  61
  62#[derive(Debug)]
  63struct IndexedCell {
  64    point: Point,
  65    cell: Cell,
  66}
  67
  68impl Deref for IndexedCell {
  69    type Target = Cell;
  70
  71    #[inline]
  72    fn deref(&self) -> &Cell {
  73        &self.cell
  74    }
  75}
  76
  77///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
  78struct DisplayCursor {
  79    line: i32,
  80    col: usize,
  81}
  82
  83impl DisplayCursor {
  84    fn from(cursor_point: Point, display_offset: usize) -> Self {
  85        Self {
  86            line: cursor_point.line.0 + display_offset as i32,
  87            col: cursor_point.column.0,
  88        }
  89    }
  90
  91    pub fn line(&self) -> i32 {
  92        self.line
  93    }
  94
  95    pub fn col(&self) -> usize {
  96        self.col
  97    }
  98}
  99
 100#[derive(Clone, Debug, Default)]
 101struct LayoutCell {
 102    point: Point<i32, i32>,
 103    text: Line,
 104}
 105
 106impl LayoutCell {
 107    fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
 108        LayoutCell { point, text }
 109    }
 110
 111    fn paint(
 112        &self,
 113        origin: Vector2F,
 114        layout: &LayoutState,
 115        visible_bounds: RectF,
 116        cx: &mut PaintContext,
 117    ) {
 118        let pos = {
 119            let point = self.point;
 120            vec2f(
 121                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
 122                origin.y() + point.line as f32 * layout.size.line_height,
 123            )
 124        };
 125
 126        self.text
 127            .paint(pos, visible_bounds, layout.size.line_height, cx);
 128    }
 129}
 130
 131#[derive(Clone, Debug, Default)]
 132struct LayoutRect {
 133    point: Point<i32, i32>,
 134    num_of_cells: usize,
 135    color: Color,
 136}
 137
 138impl LayoutRect {
 139    fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
 140        LayoutRect {
 141            point,
 142            num_of_cells,
 143            color,
 144        }
 145    }
 146
 147    fn extend(&self) -> Self {
 148        LayoutRect {
 149            point: self.point,
 150            num_of_cells: self.num_of_cells + 1,
 151            color: self.color,
 152        }
 153    }
 154
 155    fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) {
 156        let position = {
 157            let point = self.point;
 158            vec2f(
 159                (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
 160                origin.y() + point.line as f32 * layout.size.line_height,
 161            )
 162        };
 163        let size = vec2f(
 164            (layout.size.cell_width * self.num_of_cells as f32).ceil(),
 165            layout.size.line_height,
 166        );
 167
 168        cx.scene.push_quad(Quad {
 169            bounds: RectF::new(position, size),
 170            background: Some(self.color),
 171            border: Default::default(),
 172            corner_radius: 0.,
 173        })
 174    }
 175}
 176
 177#[derive(Clone, Debug, Default)]
 178struct RelativeHighlightedRange {
 179    line_index: usize,
 180    range: Range<usize>,
 181}
 182
 183impl RelativeHighlightedRange {
 184    fn new(line_index: usize, range: Range<usize>) -> Self {
 185        RelativeHighlightedRange { line_index, range }
 186    }
 187
 188    fn to_highlighted_range_line(
 189        &self,
 190        origin: Vector2F,
 191        layout: &LayoutState,
 192    ) -> HighlightedRangeLine {
 193        let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
 194        let end_x =
 195            origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
 196
 197        HighlightedRangeLine { start_x, end_x }
 198    }
 199}
 200
 201///The GPUI element that paints the terminal.
 202///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?
 203pub struct TerminalEl {
 204    terminal: WeakModelHandle<Terminal>,
 205    view: WeakViewHandle<ConnectedView>,
 206    modal: bool,
 207    focused: bool,
 208    cursor_visible: bool,
 209}
 210
 211impl TerminalEl {
 212    pub fn new(
 213        view: WeakViewHandle<ConnectedView>,
 214        terminal: WeakModelHandle<Terminal>,
 215        modal: bool,
 216        focused: bool,
 217        cursor_visible: bool,
 218    ) -> TerminalEl {
 219        TerminalEl {
 220            view,
 221            terminal,
 222            modal,
 223            focused,
 224            cursor_visible,
 225        }
 226    }
 227
 228    fn layout_grid(
 229        grid: Vec<IndexedCell>,
 230        text_style: &TextStyle,
 231        terminal_theme: &TerminalStyle,
 232        text_layout_cache: &TextLayoutCache,
 233        font_cache: &FontCache,
 234        modal: bool,
 235        selection_range: Option<SelectionRange>,
 236    ) -> (
 237        Vec<LayoutCell>,
 238        Vec<LayoutRect>,
 239        Vec<RelativeHighlightedRange>,
 240    ) {
 241        let mut cells = vec![];
 242        let mut rects = vec![];
 243        let mut highlight_ranges = vec![];
 244
 245        let mut cur_rect: Option<LayoutRect> = None;
 246        let mut cur_alac_color = None;
 247        let mut highlighted_range = None;
 248
 249        let linegroups = grid.into_iter().group_by(|i| i.point.line);
 250        for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
 251            for (x_index, cell) in line.enumerate() {
 252                let mut fg = cell.fg;
 253                let mut bg = cell.bg;
 254                if cell.flags.contains(Flags::INVERSE) {
 255                    mem::swap(&mut fg, &mut bg);
 256                }
 257
 258                //Increase selection range
 259                {
 260                    if selection_range
 261                        .map(|range| range.contains(cell.point))
 262                        .unwrap_or(false)
 263                    {
 264                        let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
 265                        range.end = range.end.max(x_index);
 266                        highlighted_range = Some(range);
 267                    }
 268                }
 269
 270                //Expand background rect range
 271                {
 272                    if matches!(bg, Named(NamedColor::Background)) {
 273                        //Continue to next cell, resetting variables if nescessary
 274                        cur_alac_color = None;
 275                        if let Some(rect) = cur_rect {
 276                            rects.push(rect);
 277                            cur_rect = None
 278                        }
 279                    } else {
 280                        match cur_alac_color {
 281                            Some(cur_color) => {
 282                                if bg == cur_color {
 283                                    cur_rect = cur_rect.take().map(|rect| rect.extend());
 284                                } else {
 285                                    cur_alac_color = Some(bg);
 286                                    if cur_rect.is_some() {
 287                                        rects.push(cur_rect.take().unwrap());
 288                                    }
 289                                    cur_rect = Some(LayoutRect::new(
 290                                        Point::new(line_index as i32, cell.point.column.0 as i32),
 291                                        1,
 292                                        convert_color(&bg, &terminal_theme.colors, modal),
 293                                    ));
 294                                }
 295                            }
 296                            None => {
 297                                cur_alac_color = Some(bg);
 298                                cur_rect = Some(LayoutRect::new(
 299                                    Point::new(line_index as i32, cell.point.column.0 as i32),
 300                                    1,
 301                                    convert_color(&bg, &terminal_theme.colors, modal),
 302                                ));
 303                            }
 304                        }
 305                    }
 306                }
 307
 308                //Layout current cell text
 309                {
 310                    let cell_text = &cell.c.to_string();
 311                    if cell_text != " " {
 312                        let cell_style = TerminalEl::cell_style(
 313                            &cell,
 314                            fg,
 315                            terminal_theme,
 316                            text_style,
 317                            font_cache,
 318                            modal,
 319                        );
 320
 321                        let layout_cell = text_layout_cache.layout_str(
 322                            cell_text,
 323                            text_style.font_size,
 324                            &[(cell_text.len(), cell_style)],
 325                        );
 326
 327                        cells.push(LayoutCell::new(
 328                            Point::new(line_index as i32, cell.point.column.0 as i32),
 329                            layout_cell,
 330                        ))
 331                    }
 332                };
 333            }
 334
 335            if highlighted_range.is_some() {
 336                highlight_ranges.push(RelativeHighlightedRange::new(
 337                    line_index,
 338                    highlighted_range.take().unwrap(),
 339                ))
 340            }
 341
 342            if cur_rect.is_some() {
 343                rects.push(cur_rect.take().unwrap());
 344            }
 345        }
 346        (cells, rects, highlight_ranges)
 347    }
 348
 349    // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
 350    // the same position for sequential indexes. Use em_width instead
 351    fn shape_cursor(
 352        cursor_point: DisplayCursor,
 353        size: TerminalSize,
 354        text_fragment: &Line,
 355    ) -> Option<(Vector2F, f32)> {
 356        if cursor_point.line() < size.total_lines() as i32 {
 357            let cursor_width = if text_fragment.width() == 0. {
 358                size.cell_width()
 359            } else {
 360                text_fragment.width()
 361            };
 362
 363            //Cursor should always surround as much of the text as possible,
 364            //hence when on pixel boundaries round the origin down and the width up
 365            Some((
 366                vec2f(
 367                    (cursor_point.col() as f32 * size.cell_width()).floor(),
 368                    (cursor_point.line() as f32 * size.line_height()).floor(),
 369                ),
 370                cursor_width.ceil(),
 371            ))
 372        } else {
 373            None
 374        }
 375    }
 376
 377    ///Convert the Alacritty cell styles to GPUI text styles and background color
 378    fn cell_style(
 379        indexed: &IndexedCell,
 380        fg: AnsiColor,
 381        style: &TerminalStyle,
 382        text_style: &TextStyle,
 383        font_cache: &FontCache,
 384        modal: bool,
 385    ) -> RunStyle {
 386        let flags = indexed.cell.flags;
 387        let fg = convert_color(&fg, &style.colors, modal);
 388
 389        let underline = flags
 390            .intersects(Flags::ALL_UNDERLINES)
 391            .then(|| Underline {
 392                color: Some(fg),
 393                squiggly: flags.contains(Flags::UNDERCURL),
 394                thickness: OrderedFloat(1.),
 395            })
 396            .unwrap_or_default();
 397
 398        let mut properties = Properties::new();
 399        if indexed
 400            .flags
 401            .intersects(Flags::BOLD | Flags::BOLD_ITALIC | Flags::DIM_BOLD)
 402        {
 403            properties = *properties.weight(Weight::BOLD);
 404        }
 405        if indexed.flags.intersects(Flags::ITALIC | Flags::BOLD_ITALIC) {
 406            properties = *properties.style(Italic);
 407        }
 408
 409        let font_id = font_cache
 410            .select_font(text_style.font_family_id, &properties)
 411            .unwrap_or(text_style.font_id);
 412
 413        RunStyle {
 414            color: fg,
 415            font_id,
 416            underline,
 417        }
 418    }
 419
 420    fn generic_button_handler(
 421        connection: WeakModelHandle<Terminal>,
 422        origin: Vector2F,
 423        cur_size: TerminalSize,
 424        display_offset: usize,
 425        f: impl Fn(&mut Terminal, Point, Direction, MouseButtonEvent, &mut ModelContext<Terminal>),
 426    ) -> impl Fn(MouseButtonEvent, &mut EventContext) {
 427        move |event, cx| {
 428            cx.focus_parent_view();
 429            if let Some(conn_handle) = connection.upgrade(cx.app) {
 430                conn_handle.update(cx.app, |terminal, cx| {
 431                    let (point, side) = TerminalEl::mouse_to_cell_data(
 432                        event.position,
 433                        origin,
 434                        cur_size,
 435                        display_offset,
 436                    );
 437
 438                    f(terminal, point, side, event, cx);
 439
 440                    cx.notify();
 441                })
 442            }
 443        }
 444    }
 445
 446    fn attach_mouse_handlers(
 447        &self,
 448        origin: Vector2F,
 449        view_id: usize,
 450        visible_bounds: RectF,
 451        cur_size: TerminalSize,
 452        display_offset: usize,
 453        cx: &mut PaintContext,
 454    ) {
 455        let connection = self.terminal;
 456        cx.scene.push_mouse_region(
 457            MouseRegion::new(view_id, None, visible_bounds)
 458                .on_move(move |event, cx| {
 459                    if cx.is_parent_view_focused() {
 460                        if let Some(conn_handle) = connection.upgrade(cx.app) {
 461                            conn_handle.update(cx.app, |terminal, cx| {
 462                                let (point, side) = TerminalEl::mouse_to_cell_data(
 463                                    event.position,
 464                                    origin,
 465                                    cur_size,
 466                                    display_offset,
 467                                );
 468
 469                                terminal.mouse_move(point, side, &event);
 470
 471                                cx.notify();
 472                            })
 473                        }
 474                    }
 475                })
 476                .on_drag(MouseButton::Left, move |_prev, event, cx| {
 477                    if let Some(conn_handle) = connection.upgrade(cx.app) {
 478                        conn_handle.update(cx.app, |terminal, cx| {
 479                            let (point, side) = TerminalEl::mouse_to_cell_data(
 480                                event.position,
 481                                origin,
 482                                cur_size,
 483                                display_offset,
 484                            );
 485
 486                            terminal.mouse_drag(point, side);
 487
 488                            cx.notify();
 489                        })
 490                    }
 491                })
 492                .on_down(
 493                    MouseButton::Left,
 494                    TerminalEl::generic_button_handler(
 495                        connection,
 496                        origin,
 497                        cur_size,
 498                        display_offset,
 499                        move |terminal, point, side, _e, _cx| {
 500                            terminal.mouse_down(point, side);
 501                        },
 502                    ),
 503                )
 504                .on_down(
 505                    MouseButton::Right,
 506                    TerminalEl::generic_button_handler(
 507                        connection,
 508                        origin,
 509                        cur_size,
 510                        display_offset,
 511                        move |terminal, point, side, _e, _cx| {
 512                            terminal.mouse_down(point, side);
 513                        },
 514                    ),
 515                )
 516                .on_down(
 517                    MouseButton::Middle,
 518                    TerminalEl::generic_button_handler(
 519                        connection,
 520                        origin,
 521                        cur_size,
 522                        display_offset,
 523                        move |terminal, point, side, _e, _cx| {
 524                            terminal.mouse_down(point, side);
 525                        },
 526                    ),
 527                )
 528                //TODO
 529                .on_click(
 530                    MouseButton::Left,
 531                    TerminalEl::generic_button_handler(
 532                        connection,
 533                        origin,
 534                        cur_size,
 535                        display_offset,
 536                        move |terminal, point, side, e, _cx| {
 537                            terminal.click(point, side, e.click_count);
 538                        },
 539                    ),
 540                )
 541                .on_click(
 542                    MouseButton::Middle,
 543                    TerminalEl::generic_button_handler(
 544                        connection,
 545                        origin,
 546                        cur_size,
 547                        display_offset,
 548                        move |terminal, point, side, e, _cx| {
 549                            terminal.click(point, side, e.click_count);
 550                        },
 551                    ),
 552                )
 553                .on_click(
 554                    MouseButton::Right,
 555                    move |e @ MouseButtonEvent { position, .. }, cx| {
 556                        //Attempt to check the mode
 557                        if let Some(conn_handle) = connection.upgrade(cx.app) {
 558                            let handled = conn_handle.update(cx.app, |terminal, _cx| {
 559                                //Finally, we can check the mode!
 560                                if terminal.last_mode.intersects(TermMode::MOUSE_MODE) {
 561                                    let (point, side) = TerminalEl::mouse_to_cell_data(
 562                                        position,
 563                                        origin,
 564                                        cur_size,
 565                                        display_offset,
 566                                    );
 567
 568                                    terminal.click(point, side, e.click_count);
 569                                    true
 570                                } else {
 571                                    false
 572                                }
 573                            });
 574
 575                            //If I put this up by the true, then we're in the wrong 'cx'
 576                            if !handled {
 577                                cx.dispatch_action(DeployContextMenu { position });
 578                            }
 579                        } else {
 580                            cx.dispatch_action(DeployContextMenu { position });
 581                        }
 582                    },
 583                ),
 584        );
 585    }
 586
 587    ///Configures a text style from the current settings.
 588    pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
 589        // Pull the font family from settings properly overriding
 590        let family_id = settings
 591            .terminal_overrides
 592            .font_family
 593            .as_ref()
 594            .or(settings.terminal_defaults.font_family.as_ref())
 595            .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
 596            .unwrap_or(settings.buffer_font_family);
 597
 598        let font_size = settings
 599            .terminal_overrides
 600            .font_size
 601            .or(settings.terminal_defaults.font_size)
 602            .unwrap_or(settings.buffer_font_size);
 603
 604        let font_id = font_cache
 605            .select_font(family_id, &Default::default())
 606            .unwrap();
 607
 608        TextStyle {
 609            color: settings.theme.editor.text_color,
 610            font_family_id: family_id,
 611            font_family_name: font_cache.family_name(family_id).unwrap(),
 612            font_id,
 613            font_size,
 614            font_properties: Default::default(),
 615            underline: Default::default(),
 616        }
 617    }
 618
 619    pub fn mouse_to_cell_data(
 620        pos: Vector2F,
 621        origin: Vector2F,
 622        cur_size: TerminalSize,
 623        display_offset: usize,
 624    ) -> (Point, alacritty_terminal::index::Direction) {
 625        let pos = pos.sub(origin);
 626        let point = {
 627            let col = pos.x() / cur_size.cell_width; //TODO: underflow...
 628            let col = min(GridCol(col as usize), cur_size.last_column());
 629
 630            let line = pos.y() / cur_size.line_height;
 631            let line = min(line as i32, cur_size.bottommost_line().0);
 632
 633            Point::new(GridLine(line - display_offset as i32), col)
 634        };
 635
 636        //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
 637        let side = {
 638            let x = pos.0.x() as usize;
 639            let cell_x =
 640                x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
 641            let half_cell_width = (cur_size.cell_width / 2.0) as usize;
 642
 643            let additional_padding =
 644                (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
 645            let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
 646            //Width: Pixels or columns?
 647            if cell_x > half_cell_width
 648            // Edge case when mouse leaves the window.
 649            || x as f32 >= end_of_grid
 650            {
 651                Side::Right
 652            } else {
 653                Side::Left
 654            }
 655        };
 656
 657        (point, side)
 658    }
 659}
 660
 661impl Element for TerminalEl {
 662    type LayoutState = LayoutState;
 663    type PaintState = ();
 664
 665    fn layout(
 666        &mut self,
 667        constraint: gpui::SizeConstraint,
 668        cx: &mut gpui::LayoutContext,
 669    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
 670        let settings = cx.global::<Settings>();
 671        let font_cache = cx.font_cache();
 672
 673        //Setup layout information
 674        let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
 675        let text_style = TerminalEl::make_text_style(font_cache, settings);
 676        let selection_color = settings.theme.editor.selection.selection;
 677        let dimensions = {
 678            let line_height = font_cache.line_height(text_style.font_size);
 679            let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
 680            TerminalSize::new(line_height, cell_width, constraint.max)
 681        };
 682
 683        let background_color = if self.modal {
 684            terminal_theme.colors.modal_background
 685        } else {
 686            terminal_theme.colors.background
 687        };
 688
 689        let (cells, selection, cursor, display_offset, cursor_text) = self
 690            .terminal
 691            .upgrade(cx)
 692            .unwrap()
 693            .update(cx.app, |terminal, mcx| {
 694                terminal.set_size(dimensions);
 695                terminal.render_lock(mcx, |content, cursor_text| {
 696                    let mut cells = vec![];
 697                    cells.extend(
 698                        content
 699                            .display_iter
 700                            //TODO: Add this once there's a way to retain empty lines
 701                            // .filter(|ic| {
 702                            //     !ic.flags.contains(Flags::HIDDEN)
 703                            //         && !(ic.bg == Named(NamedColor::Background)
 704                            //             && ic.c == ' '
 705                            //             && !ic.flags.contains(Flags::INVERSE))
 706                            // })
 707                            .map(|ic| IndexedCell {
 708                                point: ic.point,
 709                                cell: ic.cell.clone(),
 710                            }),
 711                    );
 712
 713                    (
 714                        cells,
 715                        dbg!(content.selection),
 716                        content.cursor,
 717                        content.display_offset,
 718                        cursor_text,
 719                    )
 720                })
 721            });
 722
 723        let (cells, rects, highlights) = TerminalEl::layout_grid(
 724            cells,
 725            &text_style,
 726            &terminal_theme,
 727            cx.text_layout_cache,
 728            cx.font_cache(),
 729            self.modal,
 730            selection,
 731        );
 732
 733        //Layout cursor. Rectangle is used for IME, so we should lay it out even
 734        //if we don't end up showing it.
 735        let cursor = if let AlacCursorShape::Hidden = cursor.shape {
 736            None
 737        } else {
 738            let cursor_point = DisplayCursor::from(cursor.point, display_offset);
 739            let cursor_text = {
 740                let str_trxt = cursor_text.to_string();
 741
 742                let color = if self.focused {
 743                    terminal_theme.colors.background
 744                } else {
 745                    terminal_theme.colors.foreground
 746                };
 747
 748                cx.text_layout_cache.layout_str(
 749                    &str_trxt,
 750                    text_style.font_size,
 751                    &[(
 752                        str_trxt.len(),
 753                        RunStyle {
 754                            font_id: text_style.font_id,
 755                            color,
 756                            underline: Default::default(),
 757                        },
 758                    )],
 759                )
 760            };
 761
 762            TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map(
 763                move |(cursor_position, block_width)| {
 764                    let shape = match cursor.shape {
 765                        AlacCursorShape::Block if !self.focused => CursorShape::Hollow,
 766                        AlacCursorShape::Block => CursorShape::Block,
 767                        AlacCursorShape::Underline => CursorShape::Underscore,
 768                        AlacCursorShape::Beam => CursorShape::Bar,
 769                        AlacCursorShape::HollowBlock => CursorShape::Hollow,
 770                        //This case is handled in the if wrapping the whole cursor layout
 771                        AlacCursorShape::Hidden => unreachable!(),
 772                    };
 773
 774                    Cursor::new(
 775                        cursor_position,
 776                        block_width,
 777                        dimensions.line_height,
 778                        terminal_theme.colors.cursor,
 779                        shape,
 780                        Some(cursor_text),
 781                    )
 782                },
 783            )
 784        };
 785
 786        //Done!
 787        (
 788            constraint.max,
 789            LayoutState {
 790                cells,
 791                cursor,
 792                background_color,
 793                selection_color,
 794                size: dimensions,
 795                rects,
 796                highlights,
 797                display_offset,
 798            },
 799        )
 800    }
 801
 802    fn paint(
 803        &mut self,
 804        bounds: gpui::geometry::rect::RectF,
 805        visible_bounds: gpui::geometry::rect::RectF,
 806        layout: &mut Self::LayoutState,
 807        cx: &mut gpui::PaintContext,
 808    ) -> Self::PaintState {
 809        //Setup element stuff
 810        let clip_bounds = Some(visible_bounds);
 811
 812        cx.paint_layer(clip_bounds, |cx| {
 813            let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
 814
 815            //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
 816            self.attach_mouse_handlers(
 817                origin,
 818                self.view.id(),
 819                visible_bounds,
 820                layout.size,
 821                layout.display_offset,
 822                cx,
 823            );
 824
 825            cx.paint_layer(clip_bounds, |cx| {
 826                //Start with a background color
 827                cx.scene.push_quad(Quad {
 828                    bounds: RectF::new(bounds.origin(), bounds.size()),
 829                    background: Some(layout.background_color),
 830                    border: Default::default(),
 831                    corner_radius: 0.,
 832                });
 833
 834                for rect in &layout.rects {
 835                    rect.paint(origin, layout, cx)
 836                }
 837            });
 838
 839            //Draw Selection
 840            cx.paint_layer(clip_bounds, |cx| {
 841                let start_y = layout.highlights.get(0).map(|highlight| {
 842                    origin.y() + highlight.line_index as f32 * layout.size.line_height
 843                });
 844
 845                if let Some(y) = start_y {
 846                    let range_lines = layout
 847                        .highlights
 848                        .iter()
 849                        .map(|relative_highlight| {
 850                            relative_highlight.to_highlighted_range_line(origin, layout)
 851                        })
 852                        .collect::<Vec<HighlightedRangeLine>>();
 853
 854                    let hr = HighlightedRange {
 855                        start_y: y, //Need to change this
 856                        line_height: layout.size.line_height,
 857                        lines: range_lines,
 858                        color: layout.selection_color,
 859                        //Copied from editor. TODO: move to theme or something
 860                        corner_radius: 0.15 * layout.size.line_height,
 861                    };
 862                    hr.paint(bounds, cx.scene);
 863                }
 864            });
 865
 866            //Draw the text cells
 867            cx.paint_layer(clip_bounds, |cx| {
 868                for cell in &layout.cells {
 869                    cell.paint(origin, layout, visible_bounds, cx);
 870                }
 871            });
 872
 873            //Draw cursor
 874            if self.cursor_visible {
 875                if let Some(cursor) = &layout.cursor {
 876                    cx.paint_layer(clip_bounds, |cx| {
 877                        cursor.paint(origin, cx);
 878                    })
 879                }
 880            }
 881        });
 882    }
 883
 884    fn dispatch_event(
 885        &mut self,
 886        event: &gpui::Event,
 887        _bounds: gpui::geometry::rect::RectF,
 888        visible_bounds: gpui::geometry::rect::RectF,
 889        layout: &mut Self::LayoutState,
 890        _paint: &mut Self::PaintState,
 891        cx: &mut gpui::EventContext,
 892    ) -> bool {
 893        match event {
 894            Event::ScrollWheel(ScrollWheelEvent {
 895                delta, position, ..
 896            }) => visible_bounds
 897                .contains_point(*position)
 898                .then(|| {
 899                    let scroll_lines =
 900                        (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
 901
 902                    if let Some(terminal) = self.terminal.upgrade(cx.app) {
 903                        terminal.update(cx.app, |term, _| {
 904                            term.scroll(Scroll::Delta(scroll_lines.round() as i32))
 905                        });
 906                    }
 907
 908                    cx.notify();
 909                })
 910                .is_some(),
 911            Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
 912                if !cx.is_parent_view_focused() {
 913                    return false;
 914                }
 915
 916                //TODO Talk to keith about how to catch events emitted from an element.
 917                if let Some(view) = self.view.upgrade(cx.app) {
 918                    view.update(cx.app, |view, cx| {
 919                        view.clear_bel(cx);
 920                        view.pause_cursor_blinking(cx);
 921                    })
 922                }
 923
 924                self.terminal
 925                    .upgrade(cx.app)
 926                    .map(|model_handle| {
 927                        model_handle.update(cx.app, |term, _| term.try_keystroke(keystroke))
 928                    })
 929                    .unwrap_or(false)
 930            }
 931            _ => false,
 932        }
 933    }
 934
 935    fn metadata(&self) -> Option<&dyn std::any::Any> {
 936        None
 937    }
 938
 939    fn debug(
 940        &self,
 941        _bounds: gpui::geometry::rect::RectF,
 942        _layout: &Self::LayoutState,
 943        _paint: &Self::PaintState,
 944        _cx: &gpui::DebugContext,
 945    ) -> gpui::serde_json::Value {
 946        json!({
 947            "type": "TerminalElement",
 948        })
 949    }
 950
 951    fn rect_for_text_range(
 952        &self,
 953        _: Range<usize>,
 954        bounds: RectF,
 955        _: RectF,
 956        layout: &Self::LayoutState,
 957        _: &Self::PaintState,
 958        _: &gpui::MeasurementContext,
 959    ) -> Option<RectF> {
 960        // Use the same origin that's passed to `Cursor::paint` in the paint
 961        // method bove.
 962        let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
 963
 964        // TODO - Why is it necessary to move downward one line to get correct
 965        // positioning? I would think that we'd want the same rect that is
 966        // painted for the cursor.
 967        origin += vec2f(0., layout.size.line_height);
 968
 969        Some(layout.cursor.as_ref()?.bounding_rect(origin))
 970    }
 971}
 972
 973mod test {
 974
 975    #[test]
 976    fn test_mouse_to_selection() {
 977        let term_width = 100.;
 978        let term_height = 200.;
 979        let cell_width = 10.;
 980        let line_height = 20.;
 981        let mouse_pos_x = 100.; //Window relative
 982        let mouse_pos_y = 100.; //Window relative
 983        let origin_x = 10.;
 984        let origin_y = 20.;
 985
 986        let cur_size = crate::connected_el::TerminalSize::new(
 987            line_height,
 988            cell_width,
 989            gpui::geometry::vector::vec2f(term_width, term_height),
 990        );
 991
 992        let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
 993        let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
 994        let (point, _) =
 995            crate::connected_el::TerminalEl::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
 996        assert_eq!(
 997            point,
 998            alacritty_terminal::index::Point::new(
 999                alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
1000                alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
1001            )
1002        );
1003    }
1004}