1use alacritty_terminal::{
2 ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
3 grid::{Dimensions, Scroll},
4 index::{Column as GridCol, Direction, Line as GridLine, Point, Side},
5 selection::SelectionRange,
6 term::{
7 cell::{Cell, Flags},
8 TermMode,
9 },
10};
11use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
12use gpui::{
13 color::Color,
14 elements::*,
15 fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
16 geometry::{
17 rect::RectF,
18 vector::{vec2f, Vector2F},
19 },
20 json::json,
21 text_layout::{Line, RunStyle},
22 Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseButtonEvent,
23 MouseRegion, PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle,
24 WeakViewHandle,
25};
26use itertools::Itertools;
27use ordered_float::OrderedFloat;
28use settings::Settings;
29use theme::TerminalStyle;
30use util::ResultExt;
31
32use std::{
33 cmp::min,
34 mem,
35 ops::{Deref, Range},
36};
37use std::{fmt::Debug, ops::Sub};
38
39use crate::{
40 connected_view::{ConnectedView, DeployContextMenu},
41 mappings::colors::convert_color,
42 Terminal, TerminalSize,
43};
44
45///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
46///Scroll multiplier that is set to 3 by default. This will be removed when I
47///Implement scroll bars.
48pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
49
50///The information generated during layout that is nescessary for painting
51pub struct LayoutState {
52 cells: Vec<LayoutCell>,
53 rects: Vec<LayoutRect>,
54 highlights: Vec<RelativeHighlightedRange>,
55 cursor: Option<Cursor>,
56 background_color: Color,
57 selection_color: Color,
58 size: TerminalSize,
59 display_offset: usize,
60}
61
62#[derive(Debug)]
63struct IndexedCell {
64 point: Point,
65 cell: Cell,
66}
67
68impl Deref for IndexedCell {
69 type Target = Cell;
70
71 #[inline]
72 fn deref(&self) -> &Cell {
73 &self.cell
74 }
75}
76
77///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
78struct DisplayCursor {
79 line: i32,
80 col: usize,
81}
82
83impl DisplayCursor {
84 fn from(cursor_point: Point, display_offset: usize) -> Self {
85 Self {
86 line: cursor_point.line.0 + display_offset as i32,
87 col: cursor_point.column.0,
88 }
89 }
90
91 pub fn line(&self) -> i32 {
92 self.line
93 }
94
95 pub fn col(&self) -> usize {
96 self.col
97 }
98}
99
100#[derive(Clone, Debug, Default)]
101struct LayoutCell {
102 point: Point<i32, i32>,
103 text: Line,
104}
105
106impl LayoutCell {
107 fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
108 LayoutCell { point, text }
109 }
110
111 fn paint(
112 &self,
113 origin: Vector2F,
114 layout: &LayoutState,
115 visible_bounds: RectF,
116 cx: &mut PaintContext,
117 ) {
118 let pos = {
119 let point = self.point;
120 vec2f(
121 (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
122 origin.y() + point.line as f32 * layout.size.line_height,
123 )
124 };
125
126 self.text
127 .paint(pos, visible_bounds, layout.size.line_height, cx);
128 }
129}
130
131#[derive(Clone, Debug, Default)]
132struct LayoutRect {
133 point: Point<i32, i32>,
134 num_of_cells: usize,
135 color: Color,
136}
137
138impl LayoutRect {
139 fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
140 LayoutRect {
141 point,
142 num_of_cells,
143 color,
144 }
145 }
146
147 fn extend(&self) -> Self {
148 LayoutRect {
149 point: self.point,
150 num_of_cells: self.num_of_cells + 1,
151 color: self.color,
152 }
153 }
154
155 fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) {
156 let position = {
157 let point = self.point;
158 vec2f(
159 (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
160 origin.y() + point.line as f32 * layout.size.line_height,
161 )
162 };
163 let size = vec2f(
164 (layout.size.cell_width * self.num_of_cells as f32).ceil(),
165 layout.size.line_height,
166 );
167
168 cx.scene.push_quad(Quad {
169 bounds: RectF::new(position, size),
170 background: Some(self.color),
171 border: Default::default(),
172 corner_radius: 0.,
173 })
174 }
175}
176
177#[derive(Clone, Debug, Default)]
178struct RelativeHighlightedRange {
179 line_index: usize,
180 range: Range<usize>,
181}
182
183impl RelativeHighlightedRange {
184 fn new(line_index: usize, range: Range<usize>) -> Self {
185 RelativeHighlightedRange { line_index, range }
186 }
187
188 fn to_highlighted_range_line(
189 &self,
190 origin: Vector2F,
191 layout: &LayoutState,
192 ) -> HighlightedRangeLine {
193 let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
194 let end_x =
195 origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
196
197 HighlightedRangeLine { start_x, end_x }
198 }
199}
200
201///The GPUI element that paints the terminal.
202///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?
203pub struct TerminalEl {
204 terminal: WeakModelHandle<Terminal>,
205 view: WeakViewHandle<ConnectedView>,
206 modal: bool,
207 focused: bool,
208 cursor_visible: bool,
209}
210
211impl TerminalEl {
212 pub fn new(
213 view: WeakViewHandle<ConnectedView>,
214 terminal: WeakModelHandle<Terminal>,
215 modal: bool,
216 focused: bool,
217 cursor_visible: bool,
218 ) -> TerminalEl {
219 TerminalEl {
220 view,
221 terminal,
222 modal,
223 focused,
224 cursor_visible,
225 }
226 }
227
228 fn layout_grid(
229 grid: Vec<IndexedCell>,
230 text_style: &TextStyle,
231 terminal_theme: &TerminalStyle,
232 text_layout_cache: &TextLayoutCache,
233 font_cache: &FontCache,
234 modal: bool,
235 selection_range: Option<SelectionRange>,
236 ) -> (
237 Vec<LayoutCell>,
238 Vec<LayoutRect>,
239 Vec<RelativeHighlightedRange>,
240 ) {
241 let mut cells = vec![];
242 let mut rects = vec![];
243 let mut highlight_ranges = vec![];
244
245 let mut cur_rect: Option<LayoutRect> = None;
246 let mut cur_alac_color = None;
247 let mut highlighted_range = None;
248
249 let linegroups = grid.into_iter().group_by(|i| i.point.line);
250 for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
251 for (x_index, cell) in line.enumerate() {
252 let mut fg = cell.fg;
253 let mut bg = cell.bg;
254 if cell.flags.contains(Flags::INVERSE) {
255 mem::swap(&mut fg, &mut bg);
256 }
257
258 //Increase selection range
259 {
260 if selection_range
261 .map(|range| range.contains(cell.point))
262 .unwrap_or(false)
263 {
264 let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
265 range.end = range.end.max(x_index);
266 highlighted_range = Some(range);
267 }
268 }
269
270 //Expand background rect range
271 {
272 if matches!(bg, Named(NamedColor::Background)) {
273 //Continue to next cell, resetting variables if nescessary
274 cur_alac_color = None;
275 if let Some(rect) = cur_rect {
276 rects.push(rect);
277 cur_rect = None
278 }
279 } else {
280 match cur_alac_color {
281 Some(cur_color) => {
282 if bg == cur_color {
283 cur_rect = cur_rect.take().map(|rect| rect.extend());
284 } else {
285 cur_alac_color = Some(bg);
286 if cur_rect.is_some() {
287 rects.push(cur_rect.take().unwrap());
288 }
289 cur_rect = Some(LayoutRect::new(
290 Point::new(line_index as i32, cell.point.column.0 as i32),
291 1,
292 convert_color(&bg, &terminal_theme.colors, modal),
293 ));
294 }
295 }
296 None => {
297 cur_alac_color = Some(bg);
298 cur_rect = Some(LayoutRect::new(
299 Point::new(line_index as i32, cell.point.column.0 as i32),
300 1,
301 convert_color(&bg, &terminal_theme.colors, modal),
302 ));
303 }
304 }
305 }
306 }
307
308 //Layout current cell text
309 {
310 let cell_text = &cell.c.to_string();
311 if cell_text != " " {
312 let cell_style = TerminalEl::cell_style(
313 &cell,
314 fg,
315 terminal_theme,
316 text_style,
317 font_cache,
318 modal,
319 );
320
321 let layout_cell = text_layout_cache.layout_str(
322 cell_text,
323 text_style.font_size,
324 &[(cell_text.len(), cell_style)],
325 );
326
327 cells.push(LayoutCell::new(
328 Point::new(line_index as i32, cell.point.column.0 as i32),
329 layout_cell,
330 ))
331 }
332 };
333 }
334
335 if highlighted_range.is_some() {
336 highlight_ranges.push(RelativeHighlightedRange::new(
337 line_index,
338 highlighted_range.take().unwrap(),
339 ))
340 }
341
342 if cur_rect.is_some() {
343 rects.push(cur_rect.take().unwrap());
344 }
345 }
346 (cells, rects, highlight_ranges)
347 }
348
349 // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
350 // the same position for sequential indexes. Use em_width instead
351 fn shape_cursor(
352 cursor_point: DisplayCursor,
353 size: TerminalSize,
354 text_fragment: &Line,
355 ) -> Option<(Vector2F, f32)> {
356 if cursor_point.line() < size.total_lines() as i32 {
357 let cursor_width = if text_fragment.width() == 0. {
358 size.cell_width()
359 } else {
360 text_fragment.width()
361 };
362
363 //Cursor should always surround as much of the text as possible,
364 //hence when on pixel boundaries round the origin down and the width up
365 Some((
366 vec2f(
367 (cursor_point.col() as f32 * size.cell_width()).floor(),
368 (cursor_point.line() as f32 * size.line_height()).floor(),
369 ),
370 cursor_width.ceil(),
371 ))
372 } else {
373 None
374 }
375 }
376
377 ///Convert the Alacritty cell styles to GPUI text styles and background color
378 fn cell_style(
379 indexed: &IndexedCell,
380 fg: AnsiColor,
381 style: &TerminalStyle,
382 text_style: &TextStyle,
383 font_cache: &FontCache,
384 modal: bool,
385 ) -> RunStyle {
386 let flags = indexed.cell.flags;
387 let fg = convert_color(&fg, &style.colors, modal);
388
389 let underline = flags
390 .intersects(Flags::ALL_UNDERLINES)
391 .then(|| Underline {
392 color: Some(fg),
393 squiggly: flags.contains(Flags::UNDERCURL),
394 thickness: OrderedFloat(1.),
395 })
396 .unwrap_or_default();
397
398 let mut properties = Properties::new();
399 if indexed
400 .flags
401 .intersects(Flags::BOLD | Flags::BOLD_ITALIC | Flags::DIM_BOLD)
402 {
403 properties = *properties.weight(Weight::BOLD);
404 }
405 if indexed.flags.intersects(Flags::ITALIC | Flags::BOLD_ITALIC) {
406 properties = *properties.style(Italic);
407 }
408
409 let font_id = font_cache
410 .select_font(text_style.font_family_id, &properties)
411 .unwrap_or(text_style.font_id);
412
413 RunStyle {
414 color: fg,
415 font_id,
416 underline,
417 }
418 }
419
420 fn generic_button_handler(
421 connection: WeakModelHandle<Terminal>,
422 origin: Vector2F,
423 cur_size: TerminalSize,
424 display_offset: usize,
425 f: impl Fn(&mut Terminal, Point, Direction, MouseButtonEvent, &mut ModelContext<Terminal>),
426 ) -> impl Fn(MouseButtonEvent, &mut EventContext) {
427 move |event, cx| {
428 cx.focus_parent_view();
429 if let Some(conn_handle) = connection.upgrade(cx.app) {
430 conn_handle.update(cx.app, |terminal, cx| {
431 let (point, side) = TerminalEl::mouse_to_cell_data(
432 event.position,
433 origin,
434 cur_size,
435 display_offset,
436 );
437
438 f(terminal, point, side, event, cx);
439
440 cx.notify();
441 })
442 }
443 }
444 }
445
446 fn attach_mouse_handlers(
447 &self,
448 origin: Vector2F,
449 view_id: usize,
450 visible_bounds: RectF,
451 cur_size: TerminalSize,
452 display_offset: usize,
453 cx: &mut PaintContext,
454 ) {
455 let connection = self.terminal;
456 cx.scene.push_mouse_region(
457 MouseRegion::new(view_id, None, visible_bounds)
458 .on_move(move |event, cx| {
459 if cx.is_parent_view_focused() {
460 if let Some(conn_handle) = connection.upgrade(cx.app) {
461 conn_handle.update(cx.app, |terminal, cx| {
462 let (point, side) = TerminalEl::mouse_to_cell_data(
463 event.position,
464 origin,
465 cur_size,
466 display_offset,
467 );
468
469 terminal.mouse_move(point, side, &event);
470
471 cx.notify();
472 })
473 }
474 }
475 })
476 .on_drag(MouseButton::Left, move |_prev, event, cx| {
477 if let Some(conn_handle) = connection.upgrade(cx.app) {
478 conn_handle.update(cx.app, |terminal, cx| {
479 let (point, side) = TerminalEl::mouse_to_cell_data(
480 event.position,
481 origin,
482 cur_size,
483 display_offset,
484 );
485
486 terminal.mouse_drag(point, side);
487
488 cx.notify();
489 })
490 }
491 })
492 .on_down(
493 MouseButton::Left,
494 TerminalEl::generic_button_handler(
495 connection,
496 origin,
497 cur_size,
498 display_offset,
499 move |terminal, point, side, _e, _cx| {
500 terminal.mouse_down(point, side);
501 },
502 ),
503 )
504 .on_down(
505 MouseButton::Right,
506 TerminalEl::generic_button_handler(
507 connection,
508 origin,
509 cur_size,
510 display_offset,
511 move |terminal, point, side, _e, _cx| {
512 terminal.mouse_down(point, side);
513 },
514 ),
515 )
516 .on_down(
517 MouseButton::Middle,
518 TerminalEl::generic_button_handler(
519 connection,
520 origin,
521 cur_size,
522 display_offset,
523 move |terminal, point, side, _e, _cx| {
524 terminal.mouse_down(point, side);
525 },
526 ),
527 )
528 //TODO
529 .on_click(
530 MouseButton::Left,
531 TerminalEl::generic_button_handler(
532 connection,
533 origin,
534 cur_size,
535 display_offset,
536 move |terminal, point, side, e, _cx| {
537 terminal.click(point, side, e.click_count);
538 },
539 ),
540 )
541 .on_click(
542 MouseButton::Middle,
543 TerminalEl::generic_button_handler(
544 connection,
545 origin,
546 cur_size,
547 display_offset,
548 move |terminal, point, side, e, _cx| {
549 terminal.click(point, side, e.click_count);
550 },
551 ),
552 )
553 .on_click(
554 MouseButton::Right,
555 move |e @ MouseButtonEvent { position, .. }, cx| {
556 //Attempt to check the mode
557 if let Some(conn_handle) = connection.upgrade(cx.app) {
558 let handled = conn_handle.update(cx.app, |terminal, _cx| {
559 //Finally, we can check the mode!
560 if terminal.last_mode.intersects(TermMode::MOUSE_MODE) {
561 let (point, side) = TerminalEl::mouse_to_cell_data(
562 position,
563 origin,
564 cur_size,
565 display_offset,
566 );
567
568 terminal.click(point, side, e.click_count);
569 true
570 } else {
571 false
572 }
573 });
574
575 //If I put this up by the true, then we're in the wrong 'cx'
576 if !handled {
577 cx.dispatch_action(DeployContextMenu { position });
578 }
579 } else {
580 cx.dispatch_action(DeployContextMenu { position });
581 }
582 },
583 ),
584 );
585 }
586
587 ///Configures a text style from the current settings.
588 pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
589 // Pull the font family from settings properly overriding
590 let family_id = settings
591 .terminal_overrides
592 .font_family
593 .as_ref()
594 .or(settings.terminal_defaults.font_family.as_ref())
595 .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
596 .unwrap_or(settings.buffer_font_family);
597
598 let font_size = settings
599 .terminal_overrides
600 .font_size
601 .or(settings.terminal_defaults.font_size)
602 .unwrap_or(settings.buffer_font_size);
603
604 let font_id = font_cache
605 .select_font(family_id, &Default::default())
606 .unwrap();
607
608 TextStyle {
609 color: settings.theme.editor.text_color,
610 font_family_id: family_id,
611 font_family_name: font_cache.family_name(family_id).unwrap(),
612 font_id,
613 font_size,
614 font_properties: Default::default(),
615 underline: Default::default(),
616 }
617 }
618
619 pub fn mouse_to_cell_data(
620 pos: Vector2F,
621 origin: Vector2F,
622 cur_size: TerminalSize,
623 display_offset: usize,
624 ) -> (Point, alacritty_terminal::index::Direction) {
625 let pos = pos.sub(origin);
626 let point = {
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.line_height;
631 let line = min(line as i32, cur_size.bottommost_line().0);
632
633 Point::new(GridLine(line - display_offset as i32), col)
634 };
635
636 //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
637 let side = {
638 let x = pos.0.x() as usize;
639 let cell_x =
640 x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
641 let half_cell_width = (cur_size.cell_width / 2.0) as usize;
642
643 let additional_padding =
644 (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
645 let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
646 //Width: Pixels or columns?
647 if cell_x > half_cell_width
648 // Edge case when mouse leaves the window.
649 || x as f32 >= end_of_grid
650 {
651 Side::Right
652 } else {
653 Side::Left
654 }
655 };
656
657 (point, side)
658 }
659}
660
661impl Element for TerminalEl {
662 type LayoutState = LayoutState;
663 type PaintState = ();
664
665 fn layout(
666 &mut self,
667 constraint: gpui::SizeConstraint,
668 cx: &mut gpui::LayoutContext,
669 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
670 let settings = cx.global::<Settings>();
671 let font_cache = cx.font_cache();
672
673 //Setup layout information
674 let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
675 let text_style = TerminalEl::make_text_style(font_cache, settings);
676 let selection_color = settings.theme.editor.selection.selection;
677 let dimensions = {
678 let line_height = font_cache.line_height(text_style.font_size);
679 let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
680 TerminalSize::new(line_height, cell_width, constraint.max)
681 };
682
683 let background_color = if self.modal {
684 terminal_theme.colors.modal_background
685 } else {
686 terminal_theme.colors.background
687 };
688
689 let (cells, selection, cursor, display_offset, cursor_text) = self
690 .terminal
691 .upgrade(cx)
692 .unwrap()
693 .update(cx.app, |terminal, mcx| {
694 terminal.set_size(dimensions);
695 terminal.render_lock(mcx, |content, cursor_text| {
696 let mut cells = vec![];
697 cells.extend(
698 content
699 .display_iter
700 //TODO: Add this once there's a way to retain empty lines
701 // .filter(|ic| {
702 // !ic.flags.contains(Flags::HIDDEN)
703 // && !(ic.bg == Named(NamedColor::Background)
704 // && ic.c == ' '
705 // && !ic.flags.contains(Flags::INVERSE))
706 // })
707 .map(|ic| IndexedCell {
708 point: ic.point,
709 cell: ic.cell.clone(),
710 }),
711 );
712
713 (
714 cells,
715 dbg!(content.selection),
716 content.cursor,
717 content.display_offset,
718 cursor_text,
719 )
720 })
721 });
722
723 let (cells, rects, highlights) = TerminalEl::layout_grid(
724 cells,
725 &text_style,
726 &terminal_theme,
727 cx.text_layout_cache,
728 cx.font_cache(),
729 self.modal,
730 selection,
731 );
732
733 //Layout cursor. Rectangle is used for IME, so we should lay it out even
734 //if we don't end up showing it.
735 let cursor = if let AlacCursorShape::Hidden = cursor.shape {
736 None
737 } else {
738 let cursor_point = DisplayCursor::from(cursor.point, display_offset);
739 let cursor_text = {
740 let str_trxt = cursor_text.to_string();
741
742 let color = if self.focused {
743 terminal_theme.colors.background
744 } else {
745 terminal_theme.colors.foreground
746 };
747
748 cx.text_layout_cache.layout_str(
749 &str_trxt,
750 text_style.font_size,
751 &[(
752 str_trxt.len(),
753 RunStyle {
754 font_id: text_style.font_id,
755 color,
756 underline: Default::default(),
757 },
758 )],
759 )
760 };
761
762 TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map(
763 move |(cursor_position, block_width)| {
764 let shape = match cursor.shape {
765 AlacCursorShape::Block if !self.focused => CursorShape::Hollow,
766 AlacCursorShape::Block => CursorShape::Block,
767 AlacCursorShape::Underline => CursorShape::Underscore,
768 AlacCursorShape::Beam => CursorShape::Bar,
769 AlacCursorShape::HollowBlock => CursorShape::Hollow,
770 //This case is handled in the if wrapping the whole cursor layout
771 AlacCursorShape::Hidden => unreachable!(),
772 };
773
774 Cursor::new(
775 cursor_position,
776 block_width,
777 dimensions.line_height,
778 terminal_theme.colors.cursor,
779 shape,
780 Some(cursor_text),
781 )
782 },
783 )
784 };
785
786 //Done!
787 (
788 constraint.max,
789 LayoutState {
790 cells,
791 cursor,
792 background_color,
793 selection_color,
794 size: dimensions,
795 rects,
796 highlights,
797 display_offset,
798 },
799 )
800 }
801
802 fn paint(
803 &mut self,
804 bounds: gpui::geometry::rect::RectF,
805 visible_bounds: gpui::geometry::rect::RectF,
806 layout: &mut Self::LayoutState,
807 cx: &mut gpui::PaintContext,
808 ) -> Self::PaintState {
809 //Setup element stuff
810 let clip_bounds = Some(visible_bounds);
811
812 cx.paint_layer(clip_bounds, |cx| {
813 let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
814
815 //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
816 self.attach_mouse_handlers(
817 origin,
818 self.view.id(),
819 visible_bounds,
820 layout.size,
821 layout.display_offset,
822 cx,
823 );
824
825 cx.paint_layer(clip_bounds, |cx| {
826 //Start with a background color
827 cx.scene.push_quad(Quad {
828 bounds: RectF::new(bounds.origin(), bounds.size()),
829 background: Some(layout.background_color),
830 border: Default::default(),
831 corner_radius: 0.,
832 });
833
834 for rect in &layout.rects {
835 rect.paint(origin, layout, cx)
836 }
837 });
838
839 //Draw Selection
840 cx.paint_layer(clip_bounds, |cx| {
841 let start_y = layout.highlights.get(0).map(|highlight| {
842 origin.y() + highlight.line_index as f32 * layout.size.line_height
843 });
844
845 if let Some(y) = start_y {
846 let range_lines = layout
847 .highlights
848 .iter()
849 .map(|relative_highlight| {
850 relative_highlight.to_highlighted_range_line(origin, layout)
851 })
852 .collect::<Vec<HighlightedRangeLine>>();
853
854 let hr = HighlightedRange {
855 start_y: y, //Need to change this
856 line_height: layout.size.line_height,
857 lines: range_lines,
858 color: layout.selection_color,
859 //Copied from editor. TODO: move to theme or something
860 corner_radius: 0.15 * layout.size.line_height,
861 };
862 hr.paint(bounds, cx.scene);
863 }
864 });
865
866 //Draw the text cells
867 cx.paint_layer(clip_bounds, |cx| {
868 for cell in &layout.cells {
869 cell.paint(origin, layout, visible_bounds, cx);
870 }
871 });
872
873 //Draw cursor
874 if self.cursor_visible {
875 if let Some(cursor) = &layout.cursor {
876 cx.paint_layer(clip_bounds, |cx| {
877 cursor.paint(origin, cx);
878 })
879 }
880 }
881 });
882 }
883
884 fn dispatch_event(
885 &mut self,
886 event: &gpui::Event,
887 _bounds: gpui::geometry::rect::RectF,
888 visible_bounds: gpui::geometry::rect::RectF,
889 layout: &mut Self::LayoutState,
890 _paint: &mut Self::PaintState,
891 cx: &mut gpui::EventContext,
892 ) -> bool {
893 match event {
894 Event::ScrollWheel(ScrollWheelEvent {
895 delta, position, ..
896 }) => visible_bounds
897 .contains_point(*position)
898 .then(|| {
899 let scroll_lines =
900 (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
901
902 if let Some(terminal) = self.terminal.upgrade(cx.app) {
903 terminal.update(cx.app, |term, _| {
904 term.scroll(Scroll::Delta(scroll_lines.round() as i32))
905 });
906 }
907
908 cx.notify();
909 })
910 .is_some(),
911 Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
912 if !cx.is_parent_view_focused() {
913 return false;
914 }
915
916 //TODO Talk to keith about how to catch events emitted from an element.
917 if let Some(view) = self.view.upgrade(cx.app) {
918 view.update(cx.app, |view, cx| {
919 view.clear_bel(cx);
920 view.pause_cursor_blinking(cx);
921 })
922 }
923
924 self.terminal
925 .upgrade(cx.app)
926 .map(|model_handle| {
927 model_handle.update(cx.app, |term, _| term.try_keystroke(keystroke))
928 })
929 .unwrap_or(false)
930 }
931 _ => false,
932 }
933 }
934
935 fn metadata(&self) -> Option<&dyn std::any::Any> {
936 None
937 }
938
939 fn debug(
940 &self,
941 _bounds: gpui::geometry::rect::RectF,
942 _layout: &Self::LayoutState,
943 _paint: &Self::PaintState,
944 _cx: &gpui::DebugContext,
945 ) -> gpui::serde_json::Value {
946 json!({
947 "type": "TerminalElement",
948 })
949 }
950
951 fn rect_for_text_range(
952 &self,
953 _: Range<usize>,
954 bounds: RectF,
955 _: RectF,
956 layout: &Self::LayoutState,
957 _: &Self::PaintState,
958 _: &gpui::MeasurementContext,
959 ) -> Option<RectF> {
960 // Use the same origin that's passed to `Cursor::paint` in the paint
961 // method bove.
962 let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
963
964 // TODO - Why is it necessary to move downward one line to get correct
965 // positioning? I would think that we'd want the same rect that is
966 // painted for the cursor.
967 origin += vec2f(0., layout.size.line_height);
968
969 Some(layout.cursor.as_ref()?.bounding_rect(origin))
970 }
971}
972
973mod test {
974
975 #[test]
976 fn test_mouse_to_selection() {
977 let term_width = 100.;
978 let term_height = 200.;
979 let cell_width = 10.;
980 let line_height = 20.;
981 let mouse_pos_x = 100.; //Window relative
982 let mouse_pos_y = 100.; //Window relative
983 let origin_x = 10.;
984 let origin_y = 20.;
985
986 let cur_size = crate::connected_el::TerminalSize::new(
987 line_height,
988 cell_width,
989 gpui::geometry::vector::vec2f(term_width, term_height),
990 );
991
992 let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
993 let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
994 let (point, _) =
995 crate::connected_el::TerminalEl::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
996 assert_eq!(
997 point,
998 alacritty_terminal::index::Point::new(
999 alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
1000 alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
1001 )
1002 );
1003 }
1004}