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 println!("Painted a terminal element");
222 //Setup element stuff
223 let clip_bounds = Some(visible_bounds);
224
225 cx.paint_layer(clip_bounds, |cx| {
226 let cur_size = layout.cur_size.clone();
227 let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
228
229 //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
230 attach_mouse_handlers(
231 origin,
232 cur_size,
233 self.view.id(),
234 &layout.terminal,
235 visible_bounds,
236 cx,
237 );
238
239 cx.paint_layer(clip_bounds, |cx| {
240 //Start with a background color
241 cx.scene.push_quad(Quad {
242 bounds: RectF::new(bounds.origin(), bounds.size()),
243 background: Some(layout.background_color),
244 border: Default::default(),
245 corner_radius: 0.,
246 });
247
248 //Draw cell backgrounds
249 for layout_line in &layout.layout_lines {
250 for layout_cell in &layout_line.cells {
251 let position = vec2f(
252 origin.x() + layout_cell.point.column as f32 * layout.em_width.0,
253 origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
254 );
255 let size = vec2f(layout.em_width.0, layout.line_height.0);
256
257 cx.scene.push_quad(Quad {
258 bounds: RectF::new(position, size),
259 background: Some(layout_cell.background_color),
260 border: Default::default(),
261 corner_radius: 0.,
262 })
263 }
264 }
265 });
266
267 //Draw Selection
268 cx.paint_layer(clip_bounds, |cx| {
269 let mut highlight_y = None;
270 let highlight_lines = layout
271 .layout_lines
272 .iter()
273 .filter_map(|line| {
274 if let Some(range) = &line.highlighted_range {
275 if let None = highlight_y {
276 highlight_y = Some(
277 origin.y()
278 + line.cells[0].point.line as f32 * layout.line_height.0,
279 );
280 }
281 let start_x = origin.x()
282 + line.cells[range.start].point.column as f32 * layout.em_width.0;
283 let end_x = origin.x()
284 + line.cells[range.end].point.column as f32 * layout.em_width.0
285 + layout.em_width.0;
286
287 return Some(HighlightedRangeLine { start_x, end_x });
288 } else {
289 return None;
290 }
291 })
292 .collect::<Vec<HighlightedRangeLine>>();
293
294 if let Some(y) = highlight_y {
295 let hr = HighlightedRange {
296 start_y: y, //Need to change this
297 line_height: layout.line_height.0,
298 lines: highlight_lines,
299 color: layout.selection_color,
300 //Copied from editor. TODO: move to theme or something
301 corner_radius: 0.15 * layout.line_height.0,
302 };
303 hr.paint(bounds, cx.scene);
304 }
305 });
306
307 cx.paint_layer(clip_bounds, |cx| {
308 for layout_line in &layout.layout_lines {
309 for layout_cell in &layout_line.cells {
310 let point = layout_cell.point;
311
312 //Don't actually know the start_x for a line, until here:
313 let cell_origin = vec2f(
314 origin.x() + point.column as f32 * layout.em_width.0,
315 origin.y() + point.line as f32 * layout.line_height.0,
316 );
317
318 layout_cell.text.paint(
319 cell_origin,
320 visible_bounds,
321 layout.line_height.0,
322 cx,
323 );
324 }
325 }
326 });
327
328 //Draw cursor
329 if let Some(cursor) = &layout.cursor {
330 cx.paint_layer(clip_bounds, |cx| {
331 cursor.paint(origin, cx);
332 })
333 }
334
335 #[cfg(debug_assertions)]
336 if DEBUG_GRID {
337 cx.paint_layer(clip_bounds, |cx| {
338 draw_debug_grid(bounds, layout, cx);
339 })
340 }
341 });
342 }
343
344 fn dispatch_event(
345 &mut self,
346 event: &gpui::Event,
347 _bounds: gpui::geometry::rect::RectF,
348 visible_bounds: gpui::geometry::rect::RectF,
349 layout: &mut Self::LayoutState,
350 _paint: &mut Self::PaintState,
351 cx: &mut gpui::EventContext,
352 ) -> bool {
353 match event {
354 Event::ScrollWheel(ScrollWheelEvent {
355 delta, position, ..
356 }) => visible_bounds
357 .contains_point(*position)
358 .then(|| {
359 let vertical_scroll =
360 (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
361 cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
362 })
363 .is_some(),
364 Event::KeyDown(KeyDownEvent {
365 input: Some(input), ..
366 }) => cx
367 .is_parent_view_focused()
368 .then(|| {
369 cx.dispatch_action(Input(input.to_string()));
370 })
371 .is_some(),
372 _ => false,
373 }
374 }
375
376 fn debug(
377 &self,
378 _bounds: gpui::geometry::rect::RectF,
379 _layout: &Self::LayoutState,
380 _paint: &Self::PaintState,
381 _cx: &gpui::DebugContext,
382 ) -> gpui::serde_json::Value {
383 json!({
384 "type": "TerminalElement",
385 })
386 }
387}
388
389pub fn mouse_to_cell_data(
390 pos: Vector2F,
391 origin: Vector2F,
392 cur_size: SizeInfo,
393 display_offset: usize,
394) -> (Point, alacritty_terminal::index::Direction) {
395 let relative_pos = relative_pos(pos, origin);
396 let point = grid_cell(&relative_pos, cur_size, display_offset);
397 let side = cell_side(&relative_pos, cur_size);
398 (point, side)
399}
400
401///Configures a text style from the current settings.
402fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
403 TextStyle {
404 color: settings.theme.editor.text_color,
405 font_family_id: settings.buffer_font_family,
406 font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
407 font_id: font_cache
408 .select_font(settings.buffer_font_family, &Default::default())
409 .unwrap(),
410 font_size: settings.buffer_font_size,
411 font_properties: Default::default(),
412 underline: Default::default(),
413 }
414}
415
416///Configures a size info object from the given information.
417fn make_new_size(
418 constraint: SizeConstraint,
419 cell_width: &CellWidth,
420 line_height: &LineHeight,
421) -> SizeInfo {
422 SizeInfo::new(
423 constraint.max.x() - cell_width.0,
424 constraint.max.y(),
425 cell_width.0,
426 line_height.0,
427 0.,
428 0.,
429 false,
430 )
431}
432
433fn layout_lines(
434 grid: GridIterator<Cell>,
435 text_style: &TextStyle,
436 terminal_theme: &TerminalStyle,
437 text_layout_cache: &TextLayoutCache,
438 modal: bool,
439 selection_range: Option<SelectionRange>,
440) -> Vec<LayoutLine> {
441 let lines = grid.group_by(|i| i.point.line);
442 lines
443 .into_iter()
444 .enumerate()
445 .map(|(line_index, (_, line))| {
446 let mut highlighted_range = None;
447 let cells = line
448 .enumerate()
449 .map(|(x_index, indexed_cell)| {
450 if selection_range
451 .map(|range| range.contains(indexed_cell.point))
452 .unwrap_or(false)
453 {
454 let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
455 range.end = range.end.max(x_index);
456 highlighted_range = Some(range);
457 }
458
459 let cell_text = &indexed_cell.c.to_string();
460
461 let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
462
463 //This is where we might be able to get better performance
464 let layout_cell = text_layout_cache.layout_str(
465 cell_text,
466 text_style.font_size,
467 &[(cell_text.len(), cell_style)],
468 );
469
470 LayoutCell::new(
471 Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
472 layout_cell,
473 convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
474 )
475 })
476 .collect::<Vec<LayoutCell>>();
477
478 LayoutLine {
479 cells,
480 highlighted_range,
481 }
482 })
483 .collect::<Vec<LayoutLine>>()
484}
485
486// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
487// the same position for sequential indexes. Use em_width instead
488//TODO: This function is messy, too many arguments and too many ifs. Simplify.
489fn get_cursor_shape(
490 line: usize,
491 line_index: usize,
492 display_offset: usize,
493 line_height: &LineHeight,
494 cell_width: &CellWidth,
495 total_lines: usize,
496 text_fragment: &Line,
497) -> Option<(Vector2F, f32)> {
498 let cursor_line = line + display_offset;
499 if cursor_line <= total_lines {
500 let cursor_width = if text_fragment.width() == 0. {
501 cell_width.0
502 } else {
503 text_fragment.width()
504 };
505
506 Some((
507 vec2f(
508 line_index as f32 * cell_width.0,
509 cursor_line as f32 * line_height.0,
510 ),
511 cursor_width,
512 ))
513 } else {
514 None
515 }
516}
517
518///Convert the Alacritty cell styles to GPUI text styles and background color
519fn cell_style(
520 indexed: &Indexed<&Cell>,
521 style: &TerminalStyle,
522 text_style: &TextStyle,
523 modal: bool,
524) -> RunStyle {
525 let flags = indexed.cell.flags;
526 let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
527
528 let underline = flags
529 .contains(Flags::UNDERLINE)
530 .then(|| Underline {
531 color: Some(fg),
532 squiggly: false,
533 thickness: OrderedFloat(1.),
534 })
535 .unwrap_or_default();
536
537 RunStyle {
538 color: fg,
539 font_id: text_style.font_id,
540 underline,
541 }
542}
543
544fn attach_mouse_handlers(
545 origin: Vector2F,
546 cur_size: SizeInfo,
547 view_id: usize,
548 terminal_mutex: &Arc<FairMutex<Term<ZedListener>>>,
549 visible_bounds: RectF,
550 cx: &mut PaintContext,
551) {
552 let click_mutex = terminal_mutex.clone();
553 let drag_mutex = terminal_mutex.clone();
554 let mouse_down_mutex = terminal_mutex.clone();
555
556 cx.scene.push_mouse_region(MouseRegion {
557 view_id,
558 mouse_down: Some(Rc::new(move |pos, _| {
559 let mut term = mouse_down_mutex.lock();
560 let (point, side) = mouse_to_cell_data(
561 pos,
562 origin,
563 cur_size,
564 term.renderable_content().display_offset,
565 );
566 term.selection = Some(Selection::new(SelectionType::Simple, point, side))
567 })),
568 click: Some(Rc::new(move |pos, click_count, cx| {
569 let mut term = click_mutex.lock();
570
571 let (point, side) = mouse_to_cell_data(
572 pos,
573 origin,
574 cur_size,
575 term.renderable_content().display_offset,
576 );
577
578 let selection_type = match click_count {
579 0 => return, //This is a release
580 1 => Some(SelectionType::Simple),
581 2 => Some(SelectionType::Semantic),
582 3 => Some(SelectionType::Lines),
583 _ => None,
584 };
585
586 let selection =
587 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
588
589 term.selection = selection;
590 cx.focus_parent_view();
591 cx.notify();
592 })),
593 bounds: visible_bounds,
594 drag: Some(Rc::new(move |_delta, pos, cx| {
595 let mut term = drag_mutex.lock();
596
597 let (point, side) = mouse_to_cell_data(
598 pos,
599 origin,
600 cur_size,
601 term.renderable_content().display_offset,
602 );
603
604 if let Some(mut selection) = term.selection.take() {
605 selection.update(point, side);
606 term.selection = Some(selection);
607 }
608
609 cx.notify();
610 })),
611 ..Default::default()
612 });
613}
614
615///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
616fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side {
617 let x = pos.0.x() as usize;
618 let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize;
619 let half_cell_width = (cur_size.cell_width() / 2.0) as usize;
620
621 let additional_padding =
622 (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width();
623 let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding;
624
625 if cell_x > half_cell_width
626 // Edge case when mouse leaves the window.
627 || x as f32 >= end_of_grid
628 {
629 Side::Right
630 } else {
631 Side::Left
632 }
633}
634
635///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
636///Position is a pane-relative position. That means the top left corner of the mouse
637///Region should be (0,0)
638fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
639 let pos = pos.0;
640 let col = pos.x() / cur_size.cell_width(); //TODO: underflow...
641 let col = min(GridCol(col as usize), cur_size.last_column());
642
643 let line = pos.y() / cur_size.cell_height();
644 let line = min(line as i32, cur_size.bottommost_line().0);
645
646 //when clicking, need to ADD to get to the top left cell
647 //e.g. total_lines - viewport_height, THEN subtract display offset
648 //0 -> total_lines - viewport_height - display_offset + mouse_line
649
650 Point::new(GridLine(line - display_offset as i32), col)
651}
652
653///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
654///Display and conceptual grid.
655#[cfg(debug_assertions)]
656fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
657 let width = layout.cur_size.width();
658 let height = layout.cur_size.height();
659 //Alacritty uses 'as usize', so shall we.
660 for col in 0..(width / layout.em_width.0).round() as usize {
661 cx.scene.push_quad(Quad {
662 bounds: RectF::new(
663 bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
664 vec2f(1., height),
665 ),
666 background: Some(Color::green()),
667 border: Default::default(),
668 corner_radius: 0.,
669 });
670 }
671 for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
672 cx.scene.push_quad(Quad {
673 bounds: RectF::new(
674 bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
675 vec2f(width, 1.),
676 ),
677 background: Some(Color::green()),
678 border: Default::default(),
679 corner_radius: 0.,
680 });
681 }
682}
683
684mod test {
685
686 #[test]
687 fn test_mouse_to_selection() {
688 let term_width = 100.;
689 let term_height = 200.;
690 let cell_width = 10.;
691 let line_height = 20.;
692 let mouse_pos_x = 100.; //Window relative
693 let mouse_pos_y = 100.; //Window relative
694 let origin_x = 10.;
695 let origin_y = 20.;
696
697 let cur_size = alacritty_terminal::term::SizeInfo::new(
698 term_width,
699 term_height,
700 cell_width,
701 line_height,
702 0.,
703 0.,
704 false,
705 );
706
707 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
708 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
709 let (point, _) =
710 crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
711 assert_eq!(
712 point,
713 alacritty_terminal::index::Point::new(
714 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
715 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
716 )
717 );
718 }
719
720 #[test]
721 fn test_mouse_to_selection_off_edge() {
722 let term_width = 100.;
723 let term_height = 200.;
724 let cell_width = 10.;
725 let line_height = 20.;
726 let mouse_pos_x = 100.; //Window relative
727 let mouse_pos_y = 100.; //Window relative
728 let origin_x = 10.;
729 let origin_y = 20.;
730
731 let cur_size = alacritty_terminal::term::SizeInfo::new(
732 term_width,
733 term_height,
734 cell_width,
735 line_height,
736 0.,
737 0.,
738 false,
739 );
740
741 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
742 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
743 let (point, _) =
744 crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
745 assert_eq!(
746 point,
747 alacritty_terminal::index::Point::new(
748 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
749 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
750 )
751 );
752 }
753}