terminal_element.rs

  1use alacritty_terminal::{
  2    grid::{Dimensions, GridIterator, Indexed},
  3    index::{Column as GridCol, Line as GridLine, Point, Side},
  4    selection::{Selection, SelectionRange, SelectionType},
  5    sync::FairMutex,
  6    term::{
  7        cell::{Cell, Flags},
  8        SizeInfo,
  9    },
 10    Term,
 11};
 12use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
 13use gpui::{
 14    color::Color,
 15    elements::*,
 16    fonts::{TextStyle, Underline},
 17    geometry::{
 18        rect::RectF,
 19        vector::{vec2f, Vector2F},
 20    },
 21    json::json,
 22    text_layout::{Line, RunStyle},
 23    Event, FontCache, KeyDownEvent, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
 24    SizeConstraint, TextLayoutCache, WeakViewHandle,
 25};
 26use itertools::Itertools;
 27use ordered_float::OrderedFloat;
 28use settings::Settings;
 29use std::{cmp::min, ops::Range, rc::Rc, sync::Arc};
 30use std::{fmt::Debug, ops::Sub};
 31use theme::TerminalStyle;
 32
 33use crate::{
 34    color_translation::convert_color, gpui_func_tools::paint_layer, Input, ScrollTerminal,
 35    Terminal, ZedListener,
 36};
 37
 38///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
 39///Scroll multiplier that is set to 3 by default. This will be removed when I
 40///Implement scroll bars.
 41const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
 42
 43///Used to display the grid as passed to Alacritty and the TTY.
 44///Useful for debugging inconsistencies between behavior and display
 45#[cfg(debug_assertions)]
 46const DEBUG_GRID: bool = false;
 47
 48///The GPUI element that paints the terminal.
 49pub struct TerminalEl {
 50    view: WeakViewHandle<Terminal>,
 51}
 52
 53///New type pattern so I don't mix these two up
 54struct CellWidth(f32);
 55struct LineHeight(f32);
 56
 57struct LayoutLine {
 58    cells: Vec<LayoutCell>,
 59    highlighted_range: Option<Range<usize>>,
 60}
 61
 62///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than
 63struct PaneRelativePos(Vector2F);
 64
 65///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position
 66fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos {
 67    PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating
 68}
 69
 70#[derive(Clone, Debug, Default)]
 71struct LayoutCell {
 72    point: Point<i32, i32>,
 73    text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN!
 74    background_color: Color,
 75}
 76
 77impl LayoutCell {
 78    fn new(point: Point<i32, i32>, text: Line, background_color: Color) -> LayoutCell {
 79        LayoutCell {
 80            point,
 81            text,
 82            background_color,
 83        }
 84    }
 85}
 86
 87///The information generated during layout that is nescessary for painting
 88pub struct LayoutState {
 89    layout_lines: Vec<LayoutLine>,
 90    line_height: LineHeight,
 91    em_width: CellWidth,
 92    cursor: Option<Cursor>,
 93    background_color: Color,
 94    cur_size: SizeInfo,
 95    terminal: Arc<FairMutex<Term<ZedListener>>>,
 96    selection_color: Color,
 97}
 98
 99impl TerminalEl {
100    pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
101        TerminalEl { view }
102    }
103}
104
105impl Element for TerminalEl {
106    type LayoutState = LayoutState;
107    type PaintState = ();
108
109    fn layout(
110        &mut self,
111        constraint: gpui::SizeConstraint,
112        cx: &mut gpui::LayoutContext,
113    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
114        //Settings immutably borrows cx here for the settings and font cache
115        //and we need to modify the cx to resize the terminal. So instead of
116        //storing Settings or the font_cache(), we toss them ASAP and then reborrow later
117        let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
118        let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
119        let cell_width = CellWidth(
120            cx.font_cache()
121                .em_advance(text_style.font_id, text_style.font_size),
122        );
123        let view_handle = self.view.upgrade(cx).unwrap();
124
125        //Tell the view our new size. Requires a mutable borrow of cx and the view
126        let cur_size = make_new_size(constraint, &cell_width, &line_height);
127        //Note that set_size locks and mutates the terminal.
128        view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
129
130        //Now that we're done with the mutable portion, grab the immutable settings and view again
131        let (selection_color, terminal_theme) = {
132            let theme = &(cx.global::<Settings>()).theme;
133            (theme.editor.selection.selection, &theme.terminal)
134        };
135        let terminal_mutex = view_handle.read(cx).term.clone();
136
137        let term = terminal_mutex.lock();
138        let grid = term.grid();
139        let cursor_point = grid.cursor.point;
140        let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
141
142        let content = term.renderable_content();
143
144        let layout_lines = layout_lines(
145            content.display_iter,
146            &text_style,
147            terminal_theme,
148            cx.text_layout_cache,
149            content.selection,
150        );
151
152        let block_text = cx.text_layout_cache.layout_str(
153            &cursor_text,
154            text_style.font_size,
155            &[(
156                cursor_text.len(),
157                RunStyle {
158                    font_id: text_style.font_id,
159                    color: terminal_theme.background,
160                    underline: Default::default(),
161                },
162            )],
163        );
164
165        let cursor = get_cursor_shape(
166            content.cursor.point.line.0 as usize,
167            content.cursor.point.column.0 as usize,
168            content.display_offset,
169            &line_height,
170            &cell_width,
171            cur_size.total_lines(),
172            &block_text,
173        )
174        .map(move |(cursor_position, block_width)| {
175            let block_width = if block_width != 0.0 {
176                block_width
177            } else {
178                cell_width.0
179            };
180
181            Cursor::new(
182                cursor_position,
183                block_width,
184                line_height.0,
185                terminal_theme.cursor,
186                CursorShape::Block,
187                Some(block_text.clone()),
188            )
189        });
190        drop(term);
191
192        (
193            constraint.max,
194            LayoutState {
195                layout_lines,
196                line_height,
197                em_width: cell_width,
198                cursor,
199                cur_size,
200                background_color: terminal_theme.background,
201                terminal: terminal_mutex,
202                selection_color,
203            },
204        )
205    }
206
207    fn paint(
208        &mut self,
209        bounds: gpui::geometry::rect::RectF,
210        visible_bounds: gpui::geometry::rect::RectF,
211        layout: &mut Self::LayoutState,
212        cx: &mut gpui::PaintContext,
213    ) -> Self::PaintState {
214        //Setup element stuff
215        let clip_bounds = Some(visible_bounds);
216
217        paint_layer(cx, clip_bounds, |cx| {
218            let cur_size = layout.cur_size.clone();
219            let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
220
221            //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
222            attach_mouse_handlers(
223                origin,
224                cur_size,
225                self.view.id(),
226                &layout.terminal,
227                visible_bounds,
228                cx,
229            );
230
231            paint_layer(cx, clip_bounds, |cx| {
232                //Start with a background color
233                cx.scene.push_quad(Quad {
234                    bounds: RectF::new(bounds.origin(), bounds.size()),
235                    background: Some(layout.background_color),
236                    border: Default::default(),
237                    corner_radius: 0.,
238                });
239
240                //Draw cell backgrounds
241                for layout_line in &layout.layout_lines {
242                    for layout_cell in &layout_line.cells {
243                        let position = vec2f(
244                            origin.x() + layout_cell.point.column as f32 * layout.em_width.0,
245                            origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
246                        );
247                        let size = vec2f(layout.em_width.0, layout.line_height.0);
248
249                        cx.scene.push_quad(Quad {
250                            bounds: RectF::new(position, size),
251                            background: Some(layout_cell.background_color),
252                            border: Default::default(),
253                            corner_radius: 0.,
254                        })
255                    }
256                }
257            });
258
259            //Draw Selection
260            paint_layer(cx, clip_bounds, |cx| {
261                let mut highlight_y = None;
262                let highlight_lines = layout
263                    .layout_lines
264                    .iter()
265                    .filter_map(|line| {
266                        if let Some(range) = &line.highlighted_range {
267                            if let None = highlight_y {
268                                highlight_y = Some(
269                                    origin.y()
270                                        + line.cells[0].point.line as f32 * layout.line_height.0,
271                                );
272                            }
273                            let start_x = origin.x()
274                                + line.cells[range.start].point.column as f32 * layout.em_width.0;
275                            let end_x = origin.x()
276                                + line.cells[range.end].point.column as f32 * layout.em_width.0
277                                + layout.em_width.0;
278
279                            return Some(HighlightedRangeLine { start_x, end_x });
280                        } else {
281                            return None;
282                        }
283                    })
284                    .collect::<Vec<HighlightedRangeLine>>();
285
286                if let Some(y) = highlight_y {
287                    let hr = HighlightedRange {
288                        start_y: y, //Need to change this
289                        line_height: layout.line_height.0,
290                        lines: highlight_lines,
291                        color: layout.selection_color,
292                        //Copied from editor. TODO: move to theme or something
293                        corner_radius: 0.15 * layout.line_height.0,
294                    };
295                    hr.paint(bounds, cx.scene);
296                }
297            });
298
299            //Draw text
300            paint_layer(cx, clip_bounds, |cx| {
301                for layout_line in &layout.layout_lines {
302                    for layout_cell in &layout_line.cells {
303                        let point = layout_cell.point;
304
305                        //Don't actually know the start_x for a line, until here:
306                        let cell_origin = vec2f(
307                            origin.x() + point.column as f32 * layout.em_width.0,
308                            origin.y() + point.line as f32 * layout.line_height.0,
309                        );
310
311                        layout_cell.text.paint(
312                            cell_origin,
313                            visible_bounds,
314                            layout.line_height.0,
315                            cx,
316                        );
317                    }
318                }
319            });
320
321            //Draw cursor
322            if let Some(cursor) = &layout.cursor {
323                paint_layer(cx, clip_bounds, |cx| {
324                    cursor.paint(origin, cx);
325                })
326            }
327
328            #[cfg(debug_assertions)]
329            if DEBUG_GRID {
330                paint_layer(cx, clip_bounds, |cx| {
331                    draw_debug_grid(bounds, layout, cx);
332                });
333            }
334        });
335    }
336
337    fn dispatch_event(
338        &mut self,
339        event: &gpui::Event,
340        _bounds: gpui::geometry::rect::RectF,
341        visible_bounds: gpui::geometry::rect::RectF,
342        layout: &mut Self::LayoutState,
343        _paint: &mut Self::PaintState,
344        cx: &mut gpui::EventContext,
345    ) -> bool {
346        match event {
347            Event::ScrollWheel(ScrollWheelEvent {
348                delta, position, ..
349            }) => visible_bounds
350                .contains_point(*position)
351                .then(|| {
352                    let vertical_scroll =
353                        (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
354                    cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
355                })
356                .is_some(),
357            Event::KeyDown(KeyDownEvent {
358                input: Some(input), ..
359            }) => cx
360                .is_parent_view_focused()
361                .then(|| {
362                    cx.dispatch_action(Input(input.to_string()));
363                })
364                .is_some(),
365            _ => false,
366        }
367    }
368
369    fn debug(
370        &self,
371        _bounds: gpui::geometry::rect::RectF,
372        _layout: &Self::LayoutState,
373        _paint: &Self::PaintState,
374        _cx: &gpui::DebugContext,
375    ) -> gpui::serde_json::Value {
376        json!({
377            "type": "TerminalElement",
378        })
379    }
380}
381
382fn mouse_to_cell_data(
383    pos: Vector2F,
384    origin: Vector2F,
385    cur_size: SizeInfo,
386    display_offset: usize,
387) -> (Point, alacritty_terminal::index::Direction) {
388    let relative_pos = relative_pos(pos, origin);
389    let point = grid_cell(&relative_pos, cur_size, display_offset);
390    let side = cell_side(&relative_pos, cur_size);
391    (point, side)
392}
393
394///Configures a text style from the current settings.
395fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
396    TextStyle {
397        color: settings.theme.editor.text_color,
398        font_family_id: settings.buffer_font_family,
399        font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
400        font_id: font_cache
401            .select_font(settings.buffer_font_family, &Default::default())
402            .unwrap(),
403        font_size: settings.buffer_font_size,
404        font_properties: Default::default(),
405        underline: Default::default(),
406    }
407}
408
409///Configures a size info object from the given information.
410fn make_new_size(
411    constraint: SizeConstraint,
412    cell_width: &CellWidth,
413    line_height: &LineHeight,
414) -> SizeInfo {
415    SizeInfo::new(
416        constraint.max.x() - cell_width.0,
417        constraint.max.y(),
418        cell_width.0,
419        line_height.0,
420        0.,
421        0.,
422        false,
423    )
424}
425
426fn layout_lines(
427    grid: GridIterator<Cell>,
428    text_style: &TextStyle,
429    terminal_theme: &TerminalStyle,
430    text_layout_cache: &TextLayoutCache,
431    selection_range: Option<SelectionRange>,
432) -> Vec<LayoutLine> {
433    let lines = grid.group_by(|i| i.point.line);
434    lines
435        .into_iter()
436        .enumerate()
437        .map(|(line_index, (_, line))| {
438            let mut highlighted_range = None;
439            let cells = line
440                .enumerate()
441                .map(|(x_index, indexed_cell)| {
442                    if selection_range
443                        .map(|range| range.contains(indexed_cell.point))
444                        .unwrap_or(false)
445                    {
446                        let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
447                        range.end = range.end.max(x_index);
448                        highlighted_range = Some(range);
449                    }
450
451                    let cell_text = &indexed_cell.c.to_string();
452
453                    let cell_style = cell_style(&indexed_cell, terminal_theme, text_style);
454
455                    //This is where we might be able to get better performance
456                    let layout_cell = text_layout_cache.layout_str(
457                        cell_text,
458                        text_style.font_size,
459                        &[(cell_text.len(), cell_style)],
460                    );
461
462                    LayoutCell::new(
463                        Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
464                        layout_cell,
465                        convert_color(&indexed_cell.bg, terminal_theme),
466                    )
467                })
468                .collect::<Vec<LayoutCell>>();
469
470            LayoutLine {
471                cells,
472                highlighted_range,
473            }
474        })
475        .collect::<Vec<LayoutLine>>()
476}
477
478// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
479// the same position for sequential indexes. Use em_width instead
480//TODO: This function is messy, too many arguments and too many ifs. Simplify.
481fn get_cursor_shape(
482    line: usize,
483    line_index: usize,
484    display_offset: usize,
485    line_height: &LineHeight,
486    cell_width: &CellWidth,
487    total_lines: usize,
488    text_fragment: &Line,
489) -> Option<(Vector2F, f32)> {
490    let cursor_line = line + display_offset;
491    if cursor_line <= total_lines {
492        let cursor_width = if text_fragment.width() == 0. {
493            cell_width.0
494        } else {
495            text_fragment.width()
496        };
497
498        Some((
499            vec2f(
500                line_index as f32 * cell_width.0,
501                cursor_line as f32 * line_height.0,
502            ),
503            cursor_width,
504        ))
505    } else {
506        None
507    }
508}
509
510///Convert the Alacritty cell styles to GPUI text styles and background color
511fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &TextStyle) -> RunStyle {
512    let flags = indexed.cell.flags;
513    let fg = convert_color(&indexed.cell.fg, style);
514
515    let underline = flags
516        .contains(Flags::UNDERLINE)
517        .then(|| Underline {
518            color: Some(fg),
519            squiggly: false,
520            thickness: OrderedFloat(1.),
521        })
522        .unwrap_or_default();
523
524    RunStyle {
525        color: fg,
526        font_id: text_style.font_id,
527        underline,
528    }
529}
530
531fn attach_mouse_handlers(
532    origin: Vector2F,
533    cur_size: SizeInfo,
534    view_id: usize,
535    terminal_mutex: &Arc<FairMutex<Term<ZedListener>>>,
536    visible_bounds: RectF,
537    cx: &mut PaintContext,
538) {
539    let click_mutex = terminal_mutex.clone();
540    let drag_mutex = terminal_mutex.clone();
541    let mouse_down_mutex = terminal_mutex.clone();
542
543    cx.scene.push_mouse_region(MouseRegion {
544        view_id,
545        mouse_down: Some(Rc::new(move |pos, _| {
546            let mut term = mouse_down_mutex.lock();
547            let (point, side) = mouse_to_cell_data(
548                pos,
549                origin,
550                cur_size,
551                term.renderable_content().display_offset,
552            );
553            term.selection = Some(Selection::new(SelectionType::Simple, point, side))
554        })),
555        click: Some(Rc::new(move |pos, click_count, cx| {
556            let mut term = click_mutex.lock();
557
558            let (point, side) = mouse_to_cell_data(
559                pos,
560                origin,
561                cur_size,
562                term.renderable_content().display_offset,
563            );
564
565            let selection_type = match click_count {
566                0 => return, //This is a release
567                1 => Some(SelectionType::Simple),
568                2 => Some(SelectionType::Semantic),
569                3 => Some(SelectionType::Lines),
570                _ => None,
571            };
572
573            let selection =
574                selection_type.map(|selection_type| Selection::new(selection_type, point, side));
575
576            term.selection = selection;
577            cx.focus_parent_view();
578            cx.notify();
579        })),
580        bounds: visible_bounds,
581        drag: Some(Rc::new(move |_delta, pos, cx| {
582            let mut term = drag_mutex.lock();
583
584            let (point, side) = mouse_to_cell_data(
585                pos,
586                origin,
587                cur_size,
588                term.renderable_content().display_offset,
589            );
590
591            if let Some(mut selection) = term.selection.take() {
592                selection.update(point, side);
593                term.selection = Some(selection);
594            }
595
596            cx.notify();
597        })),
598        ..Default::default()
599    });
600}
601
602///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
603fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side {
604    let x = pos.0.x() as usize;
605    let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize;
606    let half_cell_width = (cur_size.cell_width() / 2.0) as usize;
607
608    let additional_padding =
609        (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width();
610    let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding;
611
612    if cell_x > half_cell_width
613            // Edge case when mouse leaves the window.
614            || x as f32 >= end_of_grid
615    {
616        Side::Right
617    } else {
618        Side::Left
619    }
620}
621
622///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
623///Position is a pane-relative position. That means the top left corner of the mouse
624///Region should be (0,0)
625fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
626    let pos = pos.0;
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.cell_height();
631    let line = min(line as i32, cur_size.bottommost_line().0);
632
633    //when clicking, need to ADD to get to the top left cell
634    //e.g. total_lines - viewport_height, THEN subtract display offset
635    //0 -> total_lines - viewport_height - display_offset + mouse_line
636
637    Point::new(GridLine(line - display_offset as i32), col)
638}
639
640///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
641///Display and conceptual grid.
642#[cfg(debug_assertions)]
643fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
644    let width = layout.cur_size.width();
645    let height = layout.cur_size.height();
646    //Alacritty uses 'as usize', so shall we.
647    for col in 0..(width / layout.em_width.0).round() as usize {
648        cx.scene.push_quad(Quad {
649            bounds: RectF::new(
650                bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
651                vec2f(1., height),
652            ),
653            background: Some(Color::green()),
654            border: Default::default(),
655            corner_radius: 0.,
656        });
657    }
658    for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
659        cx.scene.push_quad(Quad {
660            bounds: RectF::new(
661                bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
662                vec2f(width, 1.),
663            ),
664            background: Some(Color::green()),
665            border: Default::default(),
666            corner_radius: 0.,
667        });
668    }
669}
670
671mod test {
672
673    #[test]
674    fn test_mouse_to_selection() {
675        let term_width = 100.;
676        let term_height = 200.;
677        let cell_width = 10.;
678        let line_height = 20.;
679        let mouse_pos_x = 100.; //Window relative
680        let mouse_pos_y = 100.; //Window relative
681        let origin_x = 10.;
682        let origin_y = 20.;
683
684        let cur_size = alacritty_terminal::term::SizeInfo::new(
685            term_width,
686            term_height,
687            cell_width,
688            line_height,
689            0.,
690            0.,
691            false,
692        );
693
694        let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
695        let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
696        let (point, _) =
697            crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
698        assert_eq!(
699            point,
700            alacritty_terminal::index::Point::new(
701                alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
702                alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
703            )
704        );
705    }
706
707    #[test]
708    fn test_mouse_to_selection_off_edge() {
709        let term_width = 100.;
710        let term_height = 200.;
711        let cell_width = 10.;
712        let line_height = 20.;
713        let mouse_pos_x = 100.; //Window relative
714        let mouse_pos_y = 100.; //Window relative
715        let origin_x = 10.;
716        let origin_y = 20.;
717
718        let cur_size = alacritty_terminal::term::SizeInfo::new(
719            term_width,
720            term_height,
721            cell_width,
722            line_height,
723            0.,
724            0.,
725            false,
726        );
727
728        let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
729        let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
730        let (point, _) =
731            crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
732        assert_eq!(
733            point,
734            alacritty_terminal::index::Point::new(
735                alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
736                alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
737            )
738        );
739    }
740}