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(
364 e @ KeyDownEvent {
365 input: Some(input), ..
366 },
367 ) => {
368 dbg!(e);
369 cx.is_parent_view_focused()
370 .then(|| {
371 cx.dispatch_action(Input(input.to_string()));
372 })
373 .is_some()
374 }
375 Event::KeyDown(e) => {
376 dbg!(e);
377 false
378 }
379 _ => false,
380 }
381 }
382
383 fn debug(
384 &self,
385 _bounds: gpui::geometry::rect::RectF,
386 _layout: &Self::LayoutState,
387 _paint: &Self::PaintState,
388 _cx: &gpui::DebugContext,
389 ) -> gpui::serde_json::Value {
390 json!({
391 "type": "TerminalElement",
392 })
393 }
394}
395
396pub fn mouse_to_cell_data(
397 pos: Vector2F,
398 origin: Vector2F,
399 cur_size: SizeInfo,
400 display_offset: usize,
401) -> (Point, alacritty_terminal::index::Direction) {
402 let relative_pos = relative_pos(pos, origin);
403 let point = grid_cell(&relative_pos, cur_size, display_offset);
404 let side = cell_side(&relative_pos, cur_size);
405 (point, side)
406}
407
408///Configures a text style from the current settings.
409fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
410 TextStyle {
411 color: settings.theme.editor.text_color,
412 font_family_id: settings.buffer_font_family,
413 font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
414 font_id: font_cache
415 .select_font(settings.buffer_font_family, &Default::default())
416 .unwrap(),
417 font_size: settings.buffer_font_size,
418 font_properties: Default::default(),
419 underline: Default::default(),
420 }
421}
422
423///Configures a size info object from the given information.
424fn make_new_size(
425 constraint: SizeConstraint,
426 cell_width: &CellWidth,
427 line_height: &LineHeight,
428) -> SizeInfo {
429 SizeInfo::new(
430 constraint.max.x() - cell_width.0,
431 constraint.max.y(),
432 cell_width.0,
433 line_height.0,
434 0.,
435 0.,
436 false,
437 )
438}
439
440fn layout_lines(
441 grid: GridIterator<Cell>,
442 text_style: &TextStyle,
443 terminal_theme: &TerminalStyle,
444 text_layout_cache: &TextLayoutCache,
445 modal: bool,
446 selection_range: Option<SelectionRange>,
447) -> Vec<LayoutLine> {
448 let lines = grid.group_by(|i| i.point.line);
449 lines
450 .into_iter()
451 .enumerate()
452 .map(|(line_index, (_, line))| {
453 let mut highlighted_range = None;
454 let cells = line
455 .enumerate()
456 .map(|(x_index, indexed_cell)| {
457 if selection_range
458 .map(|range| range.contains(indexed_cell.point))
459 .unwrap_or(false)
460 {
461 let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
462 range.end = range.end.max(x_index);
463 highlighted_range = Some(range);
464 }
465
466 let cell_text = &indexed_cell.c.to_string();
467
468 let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
469
470 //This is where we might be able to get better performance
471 let layout_cell = text_layout_cache.layout_str(
472 cell_text,
473 text_style.font_size,
474 &[(cell_text.len(), cell_style)],
475 );
476
477 LayoutCell::new(
478 Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
479 layout_cell,
480 convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
481 )
482 })
483 .collect::<Vec<LayoutCell>>();
484
485 LayoutLine {
486 cells,
487 highlighted_range,
488 }
489 })
490 .collect::<Vec<LayoutLine>>()
491}
492
493// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
494// the same position for sequential indexes. Use em_width instead
495//TODO: This function is messy, too many arguments and too many ifs. Simplify.
496fn get_cursor_shape(
497 line: usize,
498 line_index: usize,
499 display_offset: usize,
500 line_height: &LineHeight,
501 cell_width: &CellWidth,
502 total_lines: usize,
503 text_fragment: &Line,
504) -> Option<(Vector2F, f32)> {
505 let cursor_line = line + display_offset;
506 if cursor_line <= total_lines {
507 let cursor_width = if text_fragment.width() == 0. {
508 cell_width.0
509 } else {
510 text_fragment.width()
511 };
512
513 Some((
514 vec2f(
515 line_index as f32 * cell_width.0,
516 cursor_line as f32 * line_height.0,
517 ),
518 cursor_width,
519 ))
520 } else {
521 None
522 }
523}
524
525///Convert the Alacritty cell styles to GPUI text styles and background color
526fn cell_style(
527 indexed: &Indexed<&Cell>,
528 style: &TerminalStyle,
529 text_style: &TextStyle,
530 modal: bool,
531) -> RunStyle {
532 let flags = indexed.cell.flags;
533 let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
534
535 let underline = flags
536 .contains(Flags::UNDERLINE)
537 .then(|| Underline {
538 color: Some(fg),
539 squiggly: false,
540 thickness: OrderedFloat(1.),
541 })
542 .unwrap_or_default();
543
544 RunStyle {
545 color: fg,
546 font_id: text_style.font_id,
547 underline,
548 }
549}
550
551fn attach_mouse_handlers(
552 origin: Vector2F,
553 cur_size: SizeInfo,
554 view_id: usize,
555 terminal_mutex: &Arc<FairMutex<Term<ZedListener>>>,
556 visible_bounds: RectF,
557 cx: &mut PaintContext,
558) {
559 let click_mutex = terminal_mutex.clone();
560 let drag_mutex = terminal_mutex.clone();
561 let mouse_down_mutex = terminal_mutex.clone();
562
563 cx.scene.push_mouse_region(MouseRegion {
564 view_id,
565 mouse_down: Some(Rc::new(move |pos, _| {
566 let mut term = mouse_down_mutex.lock();
567 let (point, side) = mouse_to_cell_data(
568 pos,
569 origin,
570 cur_size,
571 term.renderable_content().display_offset,
572 );
573 term.selection = Some(Selection::new(SelectionType::Simple, point, side))
574 })),
575 click: Some(Rc::new(move |pos, click_count, cx| {
576 let mut term = click_mutex.lock();
577
578 let (point, side) = mouse_to_cell_data(
579 pos,
580 origin,
581 cur_size,
582 term.renderable_content().display_offset,
583 );
584
585 let selection_type = match click_count {
586 0 => return, //This is a release
587 1 => Some(SelectionType::Simple),
588 2 => Some(SelectionType::Semantic),
589 3 => Some(SelectionType::Lines),
590 _ => None,
591 };
592
593 let selection =
594 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
595
596 term.selection = selection;
597 cx.focus_parent_view();
598 cx.notify();
599 })),
600 bounds: visible_bounds,
601 drag: Some(Rc::new(move |_delta, pos, cx| {
602 let mut term = drag_mutex.lock();
603
604 let (point, side) = mouse_to_cell_data(
605 pos,
606 origin,
607 cur_size,
608 term.renderable_content().display_offset,
609 );
610
611 if let Some(mut selection) = term.selection.take() {
612 selection.update(point, side);
613 term.selection = Some(selection);
614 }
615
616 cx.notify();
617 })),
618 ..Default::default()
619 });
620}
621
622///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
623fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side {
624 let x = pos.0.x() as usize;
625 let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize;
626 let half_cell_width = (cur_size.cell_width() / 2.0) as usize;
627
628 let additional_padding =
629 (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width();
630 let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding;
631
632 if cell_x > half_cell_width
633 // Edge case when mouse leaves the window.
634 || x as f32 >= end_of_grid
635 {
636 Side::Right
637 } else {
638 Side::Left
639 }
640}
641
642///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
643///Position is a pane-relative position. That means the top left corner of the mouse
644///Region should be (0,0)
645fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
646 let pos = pos.0;
647 let col = pos.x() / cur_size.cell_width(); //TODO: underflow...
648 let col = min(GridCol(col as usize), cur_size.last_column());
649
650 let line = pos.y() / cur_size.cell_height();
651 let line = min(line as i32, cur_size.bottommost_line().0);
652
653 //when clicking, need to ADD to get to the top left cell
654 //e.g. total_lines - viewport_height, THEN subtract display offset
655 //0 -> total_lines - viewport_height - display_offset + mouse_line
656
657 Point::new(GridLine(line - display_offset as i32), col)
658}
659
660///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
661///Display and conceptual grid.
662#[cfg(debug_assertions)]
663fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
664 let width = layout.cur_size.width();
665 let height = layout.cur_size.height();
666 //Alacritty uses 'as usize', so shall we.
667 for col in 0..(width / layout.em_width.0).round() as usize {
668 cx.scene.push_quad(Quad {
669 bounds: RectF::new(
670 bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
671 vec2f(1., height),
672 ),
673 background: Some(Color::green()),
674 border: Default::default(),
675 corner_radius: 0.,
676 });
677 }
678 for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
679 cx.scene.push_quad(Quad {
680 bounds: RectF::new(
681 bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
682 vec2f(width, 1.),
683 ),
684 background: Some(Color::green()),
685 border: Default::default(),
686 corner_radius: 0.,
687 });
688 }
689}
690
691mod test {
692
693 #[test]
694 fn test_mouse_to_selection() {
695 let term_width = 100.;
696 let term_height = 200.;
697 let cell_width = 10.;
698 let line_height = 20.;
699 let mouse_pos_x = 100.; //Window relative
700 let mouse_pos_y = 100.; //Window relative
701 let origin_x = 10.;
702 let origin_y = 20.;
703
704 let cur_size = alacritty_terminal::term::SizeInfo::new(
705 term_width,
706 term_height,
707 cell_width,
708 line_height,
709 0.,
710 0.,
711 false,
712 );
713
714 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
715 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
716 let (point, _) =
717 crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
718 assert_eq!(
719 point,
720 alacritty_terminal::index::Point::new(
721 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
722 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
723 )
724 );
725 }
726
727 #[test]
728 fn test_mouse_to_selection_off_edge() {
729 let term_width = 100.;
730 let term_height = 200.;
731 let cell_width = 10.;
732 let line_height = 20.;
733 let mouse_pos_x = 100.; //Window relative
734 let mouse_pos_y = 100.; //Window relative
735 let origin_x = 10.;
736 let origin_y = 20.;
737
738 let cur_size = alacritty_terminal::term::SizeInfo::new(
739 term_width,
740 term_height,
741 cell_width,
742 line_height,
743 0.,
744 0.,
745 false,
746 );
747
748 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
749 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
750 let (point, _) =
751 crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
752 assert_eq!(
753 point,
754 alacritty_terminal::index::Point::new(
755 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
756 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
757 )
758 );
759 }
760}