1pub mod terminal_layout_context;
2
3use alacritty_terminal::{
4 ansi::{Color::Named, NamedColor},
5 event::WindowSize,
6 grid::{Dimensions, GridIterator, Indexed, Scroll},
7 index::{Column as GridCol, Line as GridLine, Point, Side},
8 selection::SelectionRange,
9 term::cell::{Cell, Flags},
10};
11use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
12use gpui::{
13 color::Color,
14 elements::*,
15 fonts::{TextStyle, Underline},
16 geometry::{
17 rect::RectF,
18 vector::{vec2f, Vector2F},
19 },
20 json::json,
21 text_layout::{Line, RunStyle},
22 Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion,
23 PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle,
24 WeakViewHandle,
25};
26use itertools::Itertools;
27use ordered_float::OrderedFloat;
28use settings::Settings;
29use theme::TerminalStyle;
30use util::ResultExt;
31
32use std::{cmp::min, ops::Range};
33use std::{fmt::Debug, ops::Sub};
34
35use crate::{color_translation::convert_color, connection::Terminal, TerminalView};
36
37use self::terminal_layout_context::TerminalLayoutData;
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///The GPUI element that paints the terminal.
45///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
46pub struct TerminalEl {
47 connection: WeakModelHandle<Terminal>,
48 view: WeakViewHandle<TerminalView>,
49 modal: bool,
50}
51
52#[derive(Clone, Copy, Debug)]
53pub struct TerminalDimensions {
54 pub cell_width: f32,
55 pub line_height: f32,
56 pub height: f32,
57 pub width: f32,
58}
59
60impl TerminalDimensions {
61 pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
62 TerminalDimensions {
63 cell_width,
64 line_height,
65 width: size.x(),
66 height: size.y(),
67 }
68 }
69
70 pub fn num_lines(&self) -> usize {
71 (self.height / self.line_height).floor() as usize
72 }
73
74 pub fn num_columns(&self) -> usize {
75 (self.width / self.cell_width).floor() as usize
76 }
77
78 pub fn height(&self) -> f32 {
79 self.height
80 }
81
82 pub fn width(&self) -> f32 {
83 self.width
84 }
85
86 pub fn cell_width(&self) -> f32 {
87 self.cell_width
88 }
89
90 pub fn line_height(&self) -> f32 {
91 self.line_height
92 }
93}
94
95//TODO look at what TermSize is
96impl Into<WindowSize> for TerminalDimensions {
97 fn into(self) -> WindowSize {
98 WindowSize {
99 num_lines: self.num_lines() as u16,
100 num_cols: self.num_columns() as u16,
101 cell_width: self.cell_width() as u16,
102 cell_height: self.line_height() as u16,
103 }
104 }
105}
106
107impl Dimensions for TerminalDimensions {
108 fn total_lines(&self) -> usize {
109 self.num_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
110 }
111
112 fn screen_lines(&self) -> usize {
113 self.num_lines()
114 }
115
116 fn columns(&self) -> usize {
117 self.num_columns()
118 }
119}
120
121#[derive(Clone, Debug, Default)]
122struct LayoutCell {
123 point: Point<i32, i32>,
124 text: Line,
125}
126
127impl LayoutCell {
128 fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
129 LayoutCell { point, text }
130 }
131
132 fn paint(
133 &self,
134 origin: Vector2F,
135 layout: &LayoutState,
136 visible_bounds: RectF,
137 cx: &mut PaintContext,
138 ) {
139 let pos = point_to_absolute(origin, self.point, layout);
140 self.text
141 .paint(pos, visible_bounds, layout.size.line_height, cx);
142 }
143}
144
145#[derive(Clone, Debug, Default)]
146struct LayoutRect {
147 point: Point<i32, i32>,
148 num_of_cells: usize,
149 color: Color,
150}
151
152impl LayoutRect {
153 fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
154 LayoutRect {
155 point,
156 num_of_cells,
157 color,
158 }
159 }
160
161 fn extend(&self) -> Self {
162 LayoutRect {
163 point: self.point,
164 num_of_cells: self.num_of_cells + 1,
165 color: self.color,
166 }
167 }
168
169 fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) {
170 let position = point_to_absolute(origin, self.point, layout);
171
172 let size = vec2f(
173 (layout.size.cell_width.ceil() * self.num_of_cells as f32).ceil(),
174 layout.size.line_height,
175 );
176
177 cx.scene.push_quad(Quad {
178 bounds: RectF::new(position, size),
179 background: Some(self.color),
180 border: Default::default(),
181 corner_radius: 0.,
182 })
183 }
184}
185
186fn point_to_absolute(origin: Vector2F, point: Point<i32, i32>, layout: &LayoutState) -> Vector2F {
187 vec2f(
188 (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
189 origin.y() + point.line as f32 * layout.size.line_height,
190 )
191}
192
193#[derive(Clone, Debug, Default)]
194struct RelativeHighlightedRange {
195 line_index: usize,
196 range: Range<usize>,
197}
198
199impl RelativeHighlightedRange {
200 fn new(line_index: usize, range: Range<usize>) -> Self {
201 RelativeHighlightedRange { line_index, range }
202 }
203
204 fn to_highlighted_range_line(
205 &self,
206 origin: Vector2F,
207 layout: &LayoutState,
208 ) -> HighlightedRangeLine {
209 let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
210 let end_x =
211 origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
212
213 return HighlightedRangeLine { start_x, end_x };
214 }
215}
216
217///The information generated during layout that is nescessary for painting
218pub struct LayoutState {
219 cells: Vec<LayoutCell>,
220 rects: Vec<LayoutRect>,
221 highlights: Vec<RelativeHighlightedRange>,
222 cursor: Option<Cursor>,
223 background_color: Color,
224 selection_color: Color,
225 size: TerminalDimensions,
226}
227
228impl TerminalEl {
229 pub fn new(
230 view: WeakViewHandle<TerminalView>,
231 connection: WeakModelHandle<Terminal>,
232 modal: bool,
233 ) -> TerminalEl {
234 TerminalEl {
235 view,
236 connection,
237 modal,
238 }
239 }
240
241 fn attach_mouse_handlers(
242 &self,
243 origin: Vector2F,
244 view_id: usize,
245 visible_bounds: RectF,
246 cur_size: TerminalDimensions,
247 cx: &mut PaintContext,
248 ) {
249 let mouse_down_connection = self.connection.clone();
250 let click_connection = self.connection.clone();
251 let drag_connection = self.connection.clone();
252 cx.scene.push_mouse_region(
253 MouseRegion::new(view_id, None, visible_bounds)
254 .on_down(
255 MouseButton::Left,
256 move |MouseButtonEvent { position, .. }, cx| {
257 if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) {
258 conn_handle.update(cx.app, |terminal, cx| {
259 let (point, side) = mouse_to_cell_data(
260 position,
261 origin,
262 cur_size,
263 terminal.get_display_offset(),
264 );
265
266 terminal.mouse_down(point, side);
267
268 cx.notify();
269 })
270 }
271 },
272 )
273 .on_click(
274 MouseButton::Left,
275 move |MouseButtonEvent {
276 position,
277 click_count,
278 ..
279 },
280 cx| {
281 cx.focus_parent_view();
282 if let Some(conn_handle) = click_connection.upgrade(cx.app) {
283 conn_handle.update(cx.app, |terminal, cx| {
284 let (point, side) = mouse_to_cell_data(
285 position,
286 origin,
287 cur_size,
288 terminal.get_display_offset(),
289 );
290
291 terminal.click(point, side, click_count);
292
293 cx.notify();
294 });
295 }
296 },
297 )
298 .on_drag(
299 MouseButton::Left,
300 move |_, MouseMovedEvent { position, .. }, cx| {
301 if let Some(conn_handle) = drag_connection.upgrade(cx.app) {
302 conn_handle.update(cx.app, |terminal, cx| {
303 let (point, side) = mouse_to_cell_data(
304 position,
305 origin,
306 cur_size,
307 terminal.get_display_offset(),
308 );
309
310 terminal.drag(point, side);
311
312 cx.notify()
313 });
314 }
315 },
316 ),
317 );
318 }
319}
320
321impl Element for TerminalEl {
322 type LayoutState = LayoutState;
323 type PaintState = ();
324
325 fn layout(
326 &mut self,
327 constraint: gpui::SizeConstraint,
328 cx: &mut gpui::LayoutContext,
329 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
330 let layout =
331 TerminalLayoutData::new(cx.global::<Settings>(), &cx.font_cache(), constraint.max);
332
333 let terminal = self.connection.upgrade(cx).unwrap().read(cx);
334
335 let (cursor, cells, rects, highlights) =
336 terminal.render_lock(Some(layout.size.clone()), |content| {
337 let (cells, rects, highlights) = layout_grid(
338 content.display_iter,
339 &layout.text_style,
340 layout.terminal_theme,
341 cx.text_layout_cache,
342 self.modal,
343 content.selection,
344 );
345
346 //Layout cursor
347 let cursor = layout_cursor(
348 // grid,
349 cx.text_layout_cache,
350 &layout,
351 content.cursor.point,
352 content.display_offset,
353 constraint,
354 );
355
356 (cursor, cells, rects, highlights)
357 });
358
359 //Select background color
360 let background_color = if self.modal {
361 layout.terminal_theme.colors.modal_background
362 } else {
363 layout.terminal_theme.colors.background
364 };
365
366 //Done!
367 (
368 constraint.max,
369 LayoutState {
370 cells,
371 cursor,
372 background_color,
373 selection_color: layout.selection_color,
374 size: layout.size,
375 rects,
376 highlights,
377 },
378 )
379 }
380
381 fn paint(
382 &mut self,
383 bounds: gpui::geometry::rect::RectF,
384 visible_bounds: gpui::geometry::rect::RectF,
385 layout: &mut Self::LayoutState,
386 cx: &mut gpui::PaintContext,
387 ) -> Self::PaintState {
388 /*
389 * For paint, I want to change how mouse events are handled:
390 * - Refactor the mouse handlers to push the grid cell actions into the connection
391 * - But keep the conversion from GPUI coordinates to grid cells in the Terminal element
392 * - Switch from directly painting things, to calling 'paint' on items produced by layout
393 */
394
395 //Setup element stuff
396 let clip_bounds = Some(visible_bounds);
397
398 cx.paint_layer(clip_bounds, |cx| {
399 let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
400
401 //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
402 self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.size, cx);
403
404 cx.paint_layer(clip_bounds, |cx| {
405 //Start with a background color
406 cx.scene.push_quad(Quad {
407 bounds: RectF::new(bounds.origin(), bounds.size()),
408 background: Some(layout.background_color),
409 border: Default::default(),
410 corner_radius: 0.,
411 });
412
413 for rect in &layout.rects {
414 rect.paint(origin, &layout, cx)
415 }
416 });
417
418 //Draw Selection
419 cx.paint_layer(clip_bounds, |cx| {
420 let start_y = layout.highlights.get(0).map(|highlight| {
421 origin.y() + highlight.line_index as f32 * layout.size.line_height
422 });
423
424 if let Some(y) = start_y {
425 let range_lines = layout
426 .highlights
427 .iter()
428 .map(|relative_highlight| {
429 relative_highlight.to_highlighted_range_line(origin, layout)
430 })
431 .collect::<Vec<HighlightedRangeLine>>();
432
433 let hr = HighlightedRange {
434 start_y: y, //Need to change this
435 line_height: layout.size.line_height,
436 lines: range_lines,
437 color: layout.selection_color,
438 //Copied from editor. TODO: move to theme or something
439 corner_radius: 0.15 * layout.size.line_height,
440 };
441 hr.paint(bounds, cx.scene);
442 }
443 });
444
445 //Draw the text cells
446 cx.paint_layer(clip_bounds, |cx| {
447 for cell in &layout.cells {
448 cell.paint(origin, layout, visible_bounds, cx);
449 }
450 });
451
452 //Draw cursor
453 if let Some(cursor) = &layout.cursor {
454 cx.paint_layer(clip_bounds, |cx| {
455 cursor.paint(origin, cx);
456 })
457 }
458 });
459 }
460
461 fn dispatch_event(
462 &mut self,
463 event: &gpui::Event,
464 _bounds: gpui::geometry::rect::RectF,
465 visible_bounds: gpui::geometry::rect::RectF,
466 layout: &mut Self::LayoutState,
467 _paint: &mut Self::PaintState,
468 cx: &mut gpui::EventContext,
469 ) -> bool {
470 match event {
471 Event::ScrollWheel(ScrollWheelEvent {
472 delta, position, ..
473 }) => visible_bounds
474 .contains_point(*position)
475 .then(|| {
476 let vertical_scroll =
477 (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
478
479 self.connection.upgrade(cx.app).map(|terminal| {
480 terminal
481 .read(cx.app)
482 .scroll(Scroll::Delta(vertical_scroll.round() as i32));
483 });
484 })
485 .is_some(),
486 Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
487 if !cx.is_parent_view_focused() {
488 return false;
489 }
490
491 //TODO Talk to keith about how to catch events emitted from an element.
492 if let Some(view) = self.view.upgrade(cx.app) {
493 view.update(cx.app, |view, cx| view.clear_bel(cx))
494 }
495
496 self.connection
497 .upgrade(cx.app)
498 .map(|model_handle| model_handle.read(cx.app))
499 .map(|term| term.try_keystroke(keystroke))
500 .unwrap_or(false)
501 }
502 _ => false,
503 }
504 }
505
506 fn metadata(&self) -> Option<&dyn std::any::Any> {
507 None
508 }
509
510 fn debug(
511 &self,
512 _bounds: gpui::geometry::rect::RectF,
513 _layout: &Self::LayoutState,
514 _paint: &Self::PaintState,
515 _cx: &gpui::DebugContext,
516 ) -> gpui::serde_json::Value {
517 json!({
518 "type": "TerminalElement",
519 })
520 }
521}
522
523///TODO: Fix cursor rendering with alacritty fork
524fn layout_cursor(
525 // grid: &Grid<Cell>,
526 text_layout_cache: &TextLayoutCache,
527 tcx: &TerminalLayoutData,
528 cursor_point: Point,
529 display_offset: usize,
530 constraint: SizeConstraint,
531) -> Option<Cursor> {
532 let cursor_text = layout_cursor_text(/*grid,*/ cursor_point, text_layout_cache, tcx);
533 get_cursor_shape(
534 cursor_point.line.0 as usize,
535 cursor_point.column.0 as usize,
536 display_offset,
537 tcx.size.line_height,
538 tcx.size.cell_width,
539 (constraint.max.y() / tcx.size.line_height) as usize, //TODO
540 &cursor_text,
541 )
542 .map(move |(cursor_position, block_width)| {
543 let block_width = if block_width != 0.0 {
544 block_width
545 } else {
546 tcx.size.cell_width
547 };
548
549 Cursor::new(
550 cursor_position,
551 block_width,
552 tcx.size.line_height,
553 tcx.terminal_theme.colors.cursor,
554 CursorShape::Block,
555 Some(cursor_text.clone()),
556 )
557 })
558}
559
560fn layout_cursor_text(
561 // grid: &Grid<Cell>,
562 _cursor_point: Point,
563 text_layout_cache: &TextLayoutCache,
564 tcx: &TerminalLayoutData,
565) -> Line {
566 let cursor_text = " "; //grid[cursor_point.line][cursor_point.column].c.to_string();
567
568 text_layout_cache.layout_str(
569 &cursor_text,
570 tcx.text_style.font_size,
571 &[(
572 cursor_text.len(),
573 RunStyle {
574 font_id: tcx.text_style.font_id,
575 color: tcx.terminal_theme.colors.background,
576 underline: Default::default(),
577 },
578 )],
579 )
580}
581
582pub fn mouse_to_cell_data(
583 pos: Vector2F,
584 origin: Vector2F,
585 cur_size: TerminalDimensions,
586 display_offset: usize,
587) -> (Point, alacritty_terminal::index::Direction) {
588 let pos = pos.sub(origin);
589 let point = {
590 let col = pos.x() / cur_size.cell_width; //TODO: underflow...
591 let col = min(GridCol(col as usize), cur_size.last_column());
592
593 let line = pos.y() / cur_size.line_height;
594 let line = min(line as i32, cur_size.bottommost_line().0);
595
596 Point::new(GridLine(line - display_offset as i32), col)
597 };
598
599 //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
600 let side = {
601 let x = pos.0.x() as usize;
602 let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
603 let half_cell_width = (cur_size.cell_width / 2.0) as usize;
604
605 let additional_padding =
606 (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
607 let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
608 //Width: Pixels or columns?
609 if cell_x > half_cell_width
610 // Edge case when mouse leaves the window.
611 || x as f32 >= end_of_grid
612 {
613 Side::Right
614 } else {
615 Side::Left
616 }
617 };
618
619 (point, side)
620}
621
622fn layout_grid(
623 grid: GridIterator<Cell>,
624 text_style: &TextStyle,
625 terminal_theme: &TerminalStyle,
626 text_layout_cache: &TextLayoutCache,
627 modal: bool,
628 selection_range: Option<SelectionRange>,
629) -> (
630 Vec<LayoutCell>,
631 Vec<LayoutRect>,
632 Vec<RelativeHighlightedRange>,
633) {
634 let mut cells = vec![];
635 let mut rects = vec![];
636 let mut highlight_ranges = vec![];
637
638 let mut cur_rect: Option<LayoutRect> = None;
639 let mut cur_alac_color = None;
640 let mut highlighted_range = None;
641
642 let linegroups = grid.group_by(|i| i.point.line);
643 for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
644 for (x_index, cell) in line.enumerate() {
645 //Increase selection range
646 {
647 if selection_range
648 .map(|range| range.contains(cell.point))
649 .unwrap_or(false)
650 {
651 let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
652 range.end = range.end.max(x_index);
653 highlighted_range = Some(range);
654 }
655 }
656
657 //Expand background rect range
658 {
659 if matches!(cell.bg, Named(NamedColor::Background)) {
660 //Continue to next cell, resetting variables if nescessary
661 cur_alac_color = None;
662 if let Some(rect) = cur_rect {
663 rects.push(rect);
664 cur_rect = None
665 }
666 } else {
667 match cur_alac_color {
668 Some(cur_color) => {
669 if cell.bg == cur_color {
670 cur_rect = cur_rect.take().map(|rect| rect.extend());
671 } else {
672 cur_alac_color = Some(cell.bg);
673 if let Some(_) = cur_rect {
674 rects.push(cur_rect.take().unwrap());
675 }
676 cur_rect = Some(LayoutRect::new(
677 Point::new(line_index as i32, cell.point.column.0 as i32),
678 1,
679 convert_color(&cell.bg, &terminal_theme.colors, modal),
680 ));
681 }
682 }
683 None => {
684 cur_alac_color = Some(cell.bg);
685 cur_rect = Some(LayoutRect::new(
686 Point::new(line_index as i32, cell.point.column.0 as i32),
687 1,
688 convert_color(&cell.bg, &terminal_theme.colors, modal),
689 ));
690 }
691 }
692 }
693 }
694
695 //Layout current cell text
696 {
697 let cell_text = &cell.c.to_string();
698 if cell_text != " " {
699 let cell_style = cell_style(&cell, terminal_theme, text_style, modal);
700
701 let layout_cell = text_layout_cache.layout_str(
702 cell_text,
703 text_style.font_size,
704 &[(cell_text.len(), cell_style)],
705 );
706
707 cells.push(LayoutCell::new(
708 Point::new(line_index as i32, cell.point.column.0 as i32),
709 layout_cell,
710 ))
711 }
712 };
713 }
714
715 if highlighted_range.is_some() {
716 highlight_ranges.push(RelativeHighlightedRange::new(
717 line_index,
718 highlighted_range.take().unwrap(),
719 ))
720 }
721
722 if cur_rect.is_some() {
723 rects.push(cur_rect.take().unwrap());
724 }
725 }
726
727 (cells, rects, highlight_ranges)
728}
729
730// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
731// the same position for sequential indexes. Use em_width instead
732//TODO: This function is messy, too many arguments and too many ifs. Simplify.
733fn get_cursor_shape(
734 line: usize,
735 line_index: usize,
736 display_offset: usize,
737 line_height: f32,
738 cell_width: f32,
739 total_lines: usize,
740 text_fragment: &Line,
741) -> Option<(Vector2F, f32)> {
742 let cursor_line = line + display_offset;
743 if cursor_line <= total_lines {
744 let cursor_width = if text_fragment.width() == 0. {
745 cell_width
746 } else {
747 text_fragment.width()
748 };
749
750 Some((
751 vec2f(
752 line_index as f32 * cell_width,
753 cursor_line as f32 * line_height,
754 ),
755 cursor_width,
756 ))
757 } else {
758 None
759 }
760}
761
762///Convert the Alacritty cell styles to GPUI text styles and background color
763fn cell_style(
764 indexed: &Indexed<&Cell>,
765 style: &TerminalStyle,
766 text_style: &TextStyle,
767 modal: bool,
768) -> RunStyle {
769 let flags = indexed.cell.flags;
770 let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
771
772 let underline = flags
773 .contains(Flags::UNDERLINE)
774 .then(|| Underline {
775 color: Some(fg),
776 squiggly: false,
777 thickness: OrderedFloat(1.),
778 })
779 .unwrap_or_default();
780
781 RunStyle {
782 color: fg,
783 font_id: text_style.font_id,
784 underline,
785 }
786}
787
788mod test {
789
790 #[test]
791 fn test_mouse_to_selection() {
792 let term_width = 100.;
793 let term_height = 200.;
794 let cell_width = 10.;
795 let line_height = 20.;
796 let mouse_pos_x = 100.; //Window relative
797 let mouse_pos_y = 100.; //Window relative
798 let origin_x = 10.;
799 let origin_y = 20.;
800
801 let cur_size = crate::terminal_element::TerminalDimensions::new(
802 line_height,
803 cell_width,
804 gpui::geometry::vector::vec2f(term_width, term_height),
805 );
806
807 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
808 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
809 let (point, _) =
810 crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
811 assert_eq!(
812 point,
813 alacritty_terminal::index::Point::new(
814 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
815 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
816 )
817 );
818 }
819}