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