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