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, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache,
 24    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    display_offset: usize,
 96    terminal: Arc<FairMutex<Term<ZedListener>>>,
 97    selection_color: Color,
 98}
 99
100impl TerminalEl {
101    pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
102        TerminalEl { view }
103    }
104}
105
106impl Element for TerminalEl {
107    type LayoutState = LayoutState;
108    type PaintState = ();
109
110    fn layout(
111        &mut self,
112        constraint: gpui::SizeConstraint,
113        cx: &mut gpui::LayoutContext,
114    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
115        //Settings immutably borrows cx here for the settings and font cache
116        //and we need to modify the cx to resize the terminal. So instead of
117        //storing Settings or the font_cache(), we toss them ASAP and then reborrow later
118        let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
119        let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
120        let cell_width = CellWidth(
121            cx.font_cache()
122                .em_advance(text_style.font_id, text_style.font_size),
123        );
124        let view_handle = self.view.upgrade(cx).unwrap();
125
126        //Tell the view our new size. Requires a mutable borrow of cx and the view
127        let cur_size = make_new_size(constraint, &cell_width, &line_height);
128        //Note that set_size locks and mutates the terminal.
129        //TODO: Would be nice to lock once for the whole of layout
130        view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
131
132        //Now that we're done with the mutable portion, grab the immutable settings and view again
133        let (selection_color, terminal_theme) = {
134            let theme = &(cx.global::<Settings>()).theme;
135            (theme.editor.selection.selection, &theme.terminal)
136        };
137        let terminal_mutex = view_handle.read(cx).term.clone();
138
139        let term = terminal_mutex.lock();
140        let grid = term.grid();
141        let cursor_point = grid.cursor.point;
142        let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
143
144        let content = term.renderable_content();
145
146        let layout_lines = layout_lines(
147            content.display_iter,
148            &text_style,
149            terminal_theme,
150            cx.text_layout_cache,
151            content.selection,
152        );
153
154        let block_text = cx.text_layout_cache.layout_str(
155            &cursor_text,
156            text_style.font_size,
157            &[(
158                cursor_text.len(),
159                RunStyle {
160                    font_id: text_style.font_id,
161                    color: terminal_theme.background,
162                    underline: Default::default(),
163                },
164            )],
165        );
166
167        let cursor = get_cursor_shape(
168            content.cursor.point.line.0 as usize,
169            content.cursor.point.column.0 as usize,
170            content.display_offset,
171            &line_height,
172            &cell_width,
173            cur_size.total_lines(),
174            &block_text,
175        )
176        .map(move |(cursor_position, block_width)| {
177            let block_width = if block_width != 0.0 {
178                block_width
179            } else {
180                cell_width.0
181            };
182
183            Cursor::new(
184                cursor_position,
185                block_width,
186                line_height.0,
187                terminal_theme.cursor,
188                CursorShape::Block,
189                Some(block_text.clone()),
190            )
191        });
192        let display_offset = content.display_offset;
193        drop(term);
194
195        (
196            constraint.max,
197            LayoutState {
198                layout_lines,
199                line_height,
200                em_width: cell_width,
201                cursor,
202                cur_size,
203                background_color: terminal_theme.background,
204                display_offset,
205                terminal: terminal_mutex,
206                selection_color,
207            },
208        )
209    }
210
211    fn paint(
212        &mut self,
213        bounds: gpui::geometry::rect::RectF,
214        visible_bounds: gpui::geometry::rect::RectF,
215        layout: &mut Self::LayoutState,
216        cx: &mut gpui::PaintContext,
217    ) -> Self::PaintState {
218        //Setup element stuff
219        let clip_bounds = Some(visible_bounds);
220
221        paint_layer(cx, clip_bounds, |cx| {
222            //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
223
224            /*
225            To set a selection,
226            set the selection variable on the terminal
227
228            CLICK:
229            Get the grid point associated with this mouse click
230            And the side????? - TODO - algorithm for calculating this in Processor::cell_side
231            On single left click -> Clear selection, start empty selection
232            On double left click -> start semantic selection
233            On double triple click -> start line selection
234
235            MOUSE MOVED:
236            Find the new cell the mouse is over
237            Update the selection by calling terminal.selection.update()
238            */
239            let cur_size = layout.cur_size.clone();
240            let display_offset = layout.display_offset.clone();
241            let terminal_mutex = layout.terminal.clone();
242            let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
243
244            //TODO: Better way of doing this?
245            let mutex1 = terminal_mutex.clone();
246            let _mutex2 = terminal_mutex.clone();
247
248            cx.scene.push_mouse_region(MouseRegion {
249                view_id: self.view.id(),
250                click: Some(Rc::new(move |pos, click_count, cx| {
251                    let (point, side) = mouse_to_cell_data(pos, origin, cur_size, display_offset);
252
253                    let selection_type = match click_count {
254                        1 => Some(SelectionType::Simple),
255                        2 => Some(SelectionType::Semantic),
256                        3 => Some(SelectionType::Lines),
257                        _ => None,
258                    };
259
260                    let selection = selection_type
261                        .map(|selection_type| Selection::new(selection_type, point, side));
262
263                    let mut term = mutex1.lock();
264                    term.selection = selection;
265                    cx.focus_parent_view()
266                })),
267                bounds: visible_bounds,
268                drag: Some(Rc::new(move |_delta, _cx| {
269                    // let (point, side) = mouse_to_cell_data(pos, origin, cur_size, display_offset);
270
271                    // let mut term = mutex2.lock();
272                    // if let Some(mut selection) = term.selection.take() {
273                    //     selection.update(point, side);
274                    //     term.selection = Some(selection);
275                    // }
276                })),
277                ..Default::default()
278            });
279
280            paint_layer(cx, clip_bounds, |cx| {
281                //Start with a background color
282                cx.scene.push_quad(Quad {
283                    bounds: RectF::new(bounds.origin(), bounds.size()),
284                    background: Some(layout.background_color),
285                    border: Default::default(),
286                    corner_radius: 0.,
287                });
288
289                //Draw cell backgrounds
290                for layout_line in &layout.layout_lines {
291                    for layout_cell in &layout_line.cells {
292                        let position = vec2f(
293                            origin.x() + layout_cell.point.column as f32 * layout.em_width.0,
294                            origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
295                        );
296                        let size = vec2f(layout.em_width.0, layout.line_height.0);
297
298                        cx.scene.push_quad(Quad {
299                            bounds: RectF::new(position, size),
300                            background: Some(layout_cell.background_color),
301                            border: Default::default(),
302                            corner_radius: 0.,
303                        })
304                    }
305                }
306            });
307
308            //Draw Selection
309            paint_layer(cx, clip_bounds, |cx| {
310                let mut highlight_y = None;
311                let highlight_lines = layout
312                    .layout_lines
313                    .iter()
314                    .filter_map(|line| {
315                        if let Some(range) = &line.highlighted_range {
316                            if let None = highlight_y {
317                                highlight_y = Some(
318                                    origin.y()
319                                        + line.cells[0].point.line as f32 * layout.line_height.0,
320                                );
321                            }
322                            let start_x = origin.x()
323                                + line.cells[range.start].point.column as f32 * layout.em_width.0;
324                            let end_x = origin.x()
325                                //TODO: Why -1? I know switch from count to index... but where...
326                                + line.cells[range.end - 1].point.column as f32 * layout.em_width.0
327                                + layout.em_width.0;
328
329                            return Some(HighlightedRangeLine { start_x, end_x });
330                        } else {
331                            return None;
332                        }
333                    })
334                    .collect::<Vec<HighlightedRangeLine>>();
335
336                if let Some(y) = highlight_y {
337                    let hr = HighlightedRange {
338                        start_y: y, //Need to change this
339                        line_height: layout.line_height.0,
340                        lines: highlight_lines,
341                        color: layout.selection_color,
342                        //Copied from editor. TODO: move to theme or something
343                        corner_radius: 0.15 * layout.line_height.0,
344                    };
345                    hr.paint(bounds, cx.scene);
346                }
347            });
348
349            //Draw text
350            paint_layer(cx, clip_bounds, |cx| {
351                for layout_line in &layout.layout_lines {
352                    for layout_cell in &layout_line.cells {
353                        let point = layout_cell.point;
354
355                        //Don't actually know the start_x for a line, until here:
356                        let cell_origin = vec2f(
357                            origin.x() + point.column as f32 * layout.em_width.0,
358                            origin.y() + point.line as f32 * layout.line_height.0,
359                        );
360
361                        layout_cell.text.paint(
362                            cell_origin,
363                            visible_bounds,
364                            layout.line_height.0,
365                            cx,
366                        );
367                    }
368                }
369            });
370
371            //Draw cursor
372            if let Some(cursor) = &layout.cursor {
373                paint_layer(cx, clip_bounds, |cx| {
374                    cursor.paint(origin, cx);
375                })
376            }
377
378            #[cfg(debug_assertions)]
379            if DEBUG_GRID {
380                paint_layer(cx, clip_bounds, |cx| {
381                    draw_debug_grid(bounds, layout, cx);
382                });
383            }
384        });
385    }
386
387    fn dispatch_event(
388        &mut self,
389        event: &gpui::Event,
390        _bounds: gpui::geometry::rect::RectF,
391        visible_bounds: gpui::geometry::rect::RectF,
392        layout: &mut Self::LayoutState,
393        _paint: &mut Self::PaintState,
394        cx: &mut gpui::EventContext,
395    ) -> bool {
396        match event {
397            Event::ScrollWheel {
398                delta, position, ..
399            } => visible_bounds
400                .contains_point(*position)
401                .then(|| {
402                    let vertical_scroll =
403                        (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
404                    cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
405                })
406                .is_some(),
407            Event::KeyDown {
408                input: Some(input), ..
409            } => cx
410                .is_parent_view_focused()
411                .then(|| {
412                    cx.dispatch_action(Input(input.to_string()));
413                })
414                .is_some(),
415            _ => false,
416        }
417    }
418
419    fn debug(
420        &self,
421        _bounds: gpui::geometry::rect::RectF,
422        _layout: &Self::LayoutState,
423        _paint: &Self::PaintState,
424        _cx: &gpui::DebugContext,
425    ) -> gpui::serde_json::Value {
426        json!({
427            "type": "TerminalElement",
428        })
429    }
430}
431
432fn mouse_to_cell_data(
433    pos: Vector2F,
434    origin: Vector2F,
435    cur_size: SizeInfo,
436    display_offset: usize,
437) -> (Point, alacritty_terminal::index::Direction) {
438    let relative_pos = relative_pos(pos, origin);
439    let point = grid_cell(&relative_pos, cur_size, display_offset);
440    let side = cell_side(&relative_pos, cur_size);
441    (point, side)
442}
443
444///Configures a text style from the current settings.
445fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
446    TextStyle {
447        color: settings.theme.editor.text_color,
448        font_family_id: settings.buffer_font_family,
449        font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
450        font_id: font_cache
451            .select_font(settings.buffer_font_family, &Default::default())
452            .unwrap(),
453        font_size: settings.buffer_font_size,
454        font_properties: Default::default(),
455        underline: Default::default(),
456    }
457}
458
459///Configures a size info object from the given information.
460fn make_new_size(
461    constraint: SizeConstraint,
462    cell_width: &CellWidth,
463    line_height: &LineHeight,
464) -> SizeInfo {
465    SizeInfo::new(
466        constraint.max.x() - cell_width.0,
467        constraint.max.y(),
468        cell_width.0,
469        line_height.0,
470        0.,
471        0.,
472        false,
473    )
474}
475
476//Let's say that calculating the display is correct, that means that either calculating the highlight ranges is incorrect
477//OR calculating the click ranges is incorrect
478
479fn layout_lines(
480    grid: GridIterator<Cell>,
481    text_style: &TextStyle,
482    terminal_theme: &TerminalStyle,
483    text_layout_cache: &TextLayoutCache,
484    selection_range: Option<SelectionRange>,
485) -> Vec<LayoutLine> {
486    let lines = grid.group_by(|i| i.point.line);
487    lines
488        .into_iter()
489        .enumerate()
490        .map(|(line_index, (_, line))| {
491            let mut highlighted_range = None;
492            let cells = line
493                .enumerate()
494                .map(|(x_index, indexed_cell)| {
495                    if selection_range
496                        .map(|range| range.contains(indexed_cell.point))
497                        .unwrap_or(false)
498                    {
499                        let mut range = highlighted_range.take().unwrap_or(x_index..x_index + 1);
500                        range.end = range.end.max(x_index + 1);
501                        highlighted_range = Some(range);
502                    }
503
504                    let cell_text = &indexed_cell.c.to_string();
505
506                    let cell_style = cell_style(&indexed_cell, terminal_theme, text_style);
507
508                    //This is where we might be able to get better performance
509                    let layout_cell = text_layout_cache.layout_str(
510                        cell_text,
511                        text_style.font_size,
512                        &[(cell_text.len(), cell_style)],
513                    );
514
515                    LayoutCell::new(
516                        Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
517                        layout_cell,
518                        convert_color(&indexed_cell.bg, terminal_theme),
519                    )
520                })
521                .collect::<Vec<LayoutCell>>();
522
523            LayoutLine {
524                cells,
525                highlighted_range,
526            }
527        })
528        .collect::<Vec<LayoutLine>>()
529}
530
531// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
532// the same position for sequential indexes. Use em_width instead
533//TODO: This function is messy, too many arguments and too many ifs. Simplify.
534fn get_cursor_shape(
535    line: usize,
536    line_index: usize,
537    display_offset: usize,
538    line_height: &LineHeight,
539    cell_width: &CellWidth,
540    total_lines: usize,
541    text_fragment: &Line,
542) -> Option<(Vector2F, f32)> {
543    let cursor_line = line + display_offset;
544    if cursor_line <= total_lines {
545        let cursor_width = if text_fragment.width() == 0. {
546            cell_width.0
547        } else {
548            text_fragment.width()
549        };
550
551        Some((
552            vec2f(
553                line_index as f32 * cell_width.0,
554                cursor_line as f32 * line_height.0,
555            ),
556            cursor_width,
557        ))
558    } else {
559        None
560    }
561}
562
563///Convert the Alacritty cell styles to GPUI text styles and background color
564fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &TextStyle) -> RunStyle {
565    let flags = indexed.cell.flags;
566    let fg = convert_color(&indexed.cell.fg, style);
567
568    let underline = flags
569        .contains(Flags::UNDERLINE)
570        .then(|| Underline {
571            color: Some(fg),
572            squiggly: false,
573            thickness: OrderedFloat(1.),
574        })
575        .unwrap_or_default();
576
577    RunStyle {
578        color: fg,
579        font_id: text_style.font_id,
580        underline,
581    }
582}
583
584///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
585fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side {
586    let x = pos.0.x() as usize;
587    let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize;
588    let half_cell_width = (cur_size.cell_width() / 2.0) as usize;
589
590    let additional_padding =
591        (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width();
592    let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding;
593
594    if cell_x > half_cell_width
595            // Edge case when mouse leaves the window.
596            || x as f32 >= end_of_grid
597    {
598        Side::Right
599    } else {
600        Side::Left
601    }
602}
603
604///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
605///Position is a pane-relative position. That means the top left corner of the mouse
606///Region should be (0,0)
607fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
608    let pos = pos.0;
609    let col = pos.x() / cur_size.cell_width(); //TODO: underflow...
610    let col = min(GridCol(col as usize), cur_size.last_column());
611
612    let line = pos.y() / cur_size.cell_height();
613    let line = min(line as usize, cur_size.bottommost_line().0 as usize);
614
615    Point::new(GridLine((line - display_offset) as i32), col)
616}
617
618///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
619///Display and conceptual grid.
620#[cfg(debug_assertions)]
621fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
622    let width = layout.cur_size.width();
623    let height = layout.cur_size.height();
624    //Alacritty uses 'as usize', so shall we.
625    for col in 0..(width / layout.em_width.0).round() as usize {
626        cx.scene.push_quad(Quad {
627            bounds: RectF::new(
628                bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
629                vec2f(1., height),
630            ),
631            background: Some(Color::green()),
632            border: Default::default(),
633            corner_radius: 0.,
634        });
635    }
636    for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
637        cx.scene.push_quad(Quad {
638            bounds: RectF::new(
639                bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
640                vec2f(width, 1.),
641            ),
642            background: Some(Color::green()),
643            border: Default::default(),
644            corner_radius: 0.,
645        });
646    }
647}
648
649mod test {
650
651    #[test]
652    fn test_mouse_to_selection() {
653        let term_width = 100.;
654        let term_height = 200.;
655        let cell_width = 10.;
656        let line_height = 20.;
657        let mouse_pos_x = 100.; //Window relative
658        let mouse_pos_y = 100.; //Window relative
659        let origin_x = 10.;
660        let origin_y = 20.;
661
662        let cur_size = alacritty_terminal::term::SizeInfo::new(
663            term_width,
664            term_height,
665            cell_width,
666            line_height,
667            0.,
668            0.,
669            false,
670        );
671
672        let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
673        let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
674        let (point, _) =
675            crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
676        assert_eq!(
677            point,
678            alacritty_terminal::index::Point::new(
679                alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
680                alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
681            )
682        );
683    }
684}