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