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