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