1use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
2use gpui::{
3 div, fill, point, px, relative, AnyElement, Bounds, DispatchPhase, Element, ElementId,
4 FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, HighlightStyle, Hitbox, Hsla,
5 InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId, Model, ModelContext,
6 ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine,
7 StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UnderlineStyle,
8 WeakView, WhiteSpace, WindowContext, WindowTextSystem,
9};
10use itertools::Itertools;
11use language::CursorShape;
12use settings::Settings;
13use terminal::{
14 alacritty_terminal::{
15 grid::Dimensions,
16 index::Point as AlacPoint,
17 term::{cell::Flags, TermMode},
18 vte::ansi::{
19 Color::{self as AnsiColor, Named},
20 CursorShape as AlacCursorShape, NamedColor,
21 },
22 },
23 terminal_settings::TerminalSettings,
24 HoveredWord, IndexedCell, Terminal, TerminalContent, TerminalSize,
25};
26use theme::{ActiveTheme, Theme, ThemeSettings};
27use ui::Tooltip;
28use workspace::Workspace;
29
30use std::mem;
31use std::{fmt::Debug, ops::RangeInclusive};
32
33/// The information generated during layout that is necessary for painting.
34pub struct LayoutState {
35 hitbox: Hitbox,
36 cells: Vec<LayoutCell>,
37 rects: Vec<LayoutRect>,
38 relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
39 cursor: Option<CursorLayout>,
40 background_color: Hsla,
41 dimensions: TerminalSize,
42 mode: TermMode,
43 display_offset: usize,
44 hyperlink_tooltip: Option<AnyElement>,
45 gutter: Pixels,
46 last_hovered_word: Option<HoveredWord>,
47}
48
49/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
50struct DisplayCursor {
51 line: i32,
52 col: usize,
53}
54
55impl DisplayCursor {
56 fn from(cursor_point: AlacPoint, display_offset: usize) -> Self {
57 Self {
58 line: cursor_point.line.0 + display_offset as i32,
59 col: cursor_point.column.0,
60 }
61 }
62
63 pub fn line(&self) -> i32 {
64 self.line
65 }
66
67 pub fn col(&self) -> usize {
68 self.col
69 }
70}
71
72#[derive(Debug, Default)]
73struct LayoutCell {
74 point: AlacPoint<i32, i32>,
75 text: gpui::ShapedLine,
76}
77
78impl LayoutCell {
79 fn new(point: AlacPoint<i32, i32>, text: gpui::ShapedLine) -> LayoutCell {
80 LayoutCell { point, text }
81 }
82
83 fn paint(
84 &self,
85 origin: Point<Pixels>,
86 layout: &LayoutState,
87 _visible_bounds: Bounds<Pixels>,
88 cx: &mut WindowContext,
89 ) {
90 let pos = {
91 let point = self.point;
92
93 Point::new(
94 (origin.x + point.column as f32 * layout.dimensions.cell_width).floor(),
95 origin.y + point.line as f32 * layout.dimensions.line_height,
96 )
97 };
98
99 self.text.paint(pos, layout.dimensions.line_height, cx).ok();
100 }
101}
102
103#[derive(Clone, Debug, Default)]
104struct LayoutRect {
105 point: AlacPoint<i32, i32>,
106 num_of_cells: usize,
107 color: Hsla,
108}
109
110impl LayoutRect {
111 fn new(point: AlacPoint<i32, i32>, num_of_cells: usize, color: Hsla) -> LayoutRect {
112 LayoutRect {
113 point,
114 num_of_cells,
115 color,
116 }
117 }
118
119 fn extend(&self) -> Self {
120 LayoutRect {
121 point: self.point,
122 num_of_cells: self.num_of_cells + 1,
123 color: self.color,
124 }
125 }
126
127 fn paint(&self, origin: Point<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
128 let position = {
129 let alac_point = self.point;
130 point(
131 (origin.x + alac_point.column as f32 * layout.dimensions.cell_width).floor(),
132 origin.y + alac_point.line as f32 * layout.dimensions.line_height,
133 )
134 };
135 let size = point(
136 (layout.dimensions.cell_width * self.num_of_cells as f32).ceil(),
137 layout.dimensions.line_height,
138 )
139 .into();
140
141 cx.paint_quad(fill(Bounds::new(position, size), self.color));
142 }
143}
144
145/// The GPUI element that paints the terminal.
146/// 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?
147pub struct TerminalElement {
148 terminal: Model<Terminal>,
149 workspace: WeakView<Workspace>,
150 focus: FocusHandle,
151 focused: bool,
152 cursor_visible: bool,
153 can_navigate_to_selected_word: bool,
154 interactivity: Interactivity,
155}
156
157impl InteractiveElement for TerminalElement {
158 fn interactivity(&mut self) -> &mut Interactivity {
159 &mut self.interactivity
160 }
161}
162
163impl StatefulInteractiveElement for TerminalElement {}
164
165impl TerminalElement {
166 pub fn new(
167 terminal: Model<Terminal>,
168 workspace: WeakView<Workspace>,
169 focus: FocusHandle,
170 focused: bool,
171 cursor_visible: bool,
172 can_navigate_to_selected_word: bool,
173 ) -> TerminalElement {
174 TerminalElement {
175 terminal,
176 workspace,
177 focused,
178 focus: focus.clone(),
179 cursor_visible,
180 can_navigate_to_selected_word,
181 interactivity: Default::default(),
182 }
183 .track_focus(&focus)
184 .element
185 }
186
187 //Vec<Range<AlacPoint>> -> Clip out the parts of the ranges
188
189 fn layout_grid(
190 grid: &Vec<IndexedCell>,
191 text_style: &TextStyle,
192 // terminal_theme: &TerminalStyle,
193 text_system: &WindowTextSystem,
194 hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
195 cx: &WindowContext<'_>,
196 ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
197 let theme = cx.theme();
198 let mut cells = vec![];
199 let mut rects = vec![];
200
201 let mut cur_rect: Option<LayoutRect> = None;
202 let mut cur_alac_color = None;
203
204 let linegroups = grid.into_iter().group_by(|i| i.point.line);
205 for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
206 for cell in line {
207 let mut fg = cell.fg;
208 let mut bg = cell.bg;
209 if cell.flags.contains(Flags::INVERSE) {
210 mem::swap(&mut fg, &mut bg);
211 }
212
213 //Expand background rect range
214 {
215 if matches!(bg, Named(NamedColor::Background)) {
216 //Continue to next cell, resetting variables if necessary
217 cur_alac_color = None;
218 if let Some(rect) = cur_rect {
219 rects.push(rect);
220 cur_rect = None
221 }
222 } else {
223 match cur_alac_color {
224 Some(cur_color) => {
225 if bg == cur_color {
226 // `cur_rect` can be None if it was moved to the `rects` vec after wrapping around
227 // from one line to the next. The variables are all set correctly but there is no current
228 // rect, so we create one if necessary.
229 cur_rect = cur_rect.map_or_else(
230 || {
231 Some(LayoutRect::new(
232 AlacPoint::new(
233 line_index as i32,
234 cell.point.column.0 as i32,
235 ),
236 1,
237 convert_color(&bg, theme),
238 ))
239 },
240 |rect| Some(rect.extend()),
241 );
242 } else {
243 cur_alac_color = Some(bg);
244 if cur_rect.is_some() {
245 rects.push(cur_rect.take().unwrap());
246 }
247 cur_rect = Some(LayoutRect::new(
248 AlacPoint::new(
249 line_index as i32,
250 cell.point.column.0 as i32,
251 ),
252 1,
253 convert_color(&bg, theme),
254 ));
255 }
256 }
257 None => {
258 cur_alac_color = Some(bg);
259 cur_rect = Some(LayoutRect::new(
260 AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
261 1,
262 convert_color(&bg, &theme),
263 ));
264 }
265 }
266 }
267 }
268
269 //Layout current cell text
270 {
271 if !is_blank(&cell) {
272 let cell_text = cell.c.to_string();
273 let cell_style =
274 TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink);
275
276 let layout_cell = text_system
277 .shape_line(
278 cell_text.into(),
279 text_style.font_size.to_pixels(cx.rem_size()),
280 &[cell_style],
281 )
282 .unwrap();
283
284 cells.push(LayoutCell::new(
285 AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
286 layout_cell,
287 ))
288 };
289 }
290 }
291
292 if cur_rect.is_some() {
293 rects.push(cur_rect.take().unwrap());
294 }
295 }
296 (cells, rects)
297 }
298
299 /// Computes the cursor position and expected block width, may return a zero width if x_for_index returns
300 /// the same position for sequential indexes. Use em_width instead
301 fn shape_cursor(
302 cursor_point: DisplayCursor,
303 size: TerminalSize,
304 text_fragment: &ShapedLine,
305 ) -> Option<(Point<Pixels>, Pixels)> {
306 if cursor_point.line() < size.total_lines() as i32 {
307 let cursor_width = if text_fragment.width == Pixels::ZERO {
308 size.cell_width()
309 } else {
310 text_fragment.width
311 };
312
313 // Cursor should always surround as much of the text as possible,
314 // hence when on pixel boundaries round the origin down and the width up
315 Some((
316 point(
317 (cursor_point.col() as f32 * size.cell_width()).floor(),
318 (cursor_point.line() as f32 * size.line_height()).floor(),
319 ),
320 cursor_width.ceil(),
321 ))
322 } else {
323 None
324 }
325 }
326
327 /// Converts the Alacritty cell styles to GPUI text styles and background color.
328 fn cell_style(
329 indexed: &IndexedCell,
330 fg: terminal::alacritty_terminal::vte::ansi::Color,
331 // bg: terminal::alacritty_terminal::ansi::Color,
332 colors: &Theme,
333 text_style: &TextStyle,
334 hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
335 ) -> TextRun {
336 let flags = indexed.cell.flags;
337 let mut fg = convert_color(&fg, &colors);
338
339 // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty
340 // uses 0.75. We're using 0.7 because it's pretty well in the middle of that.
341 if flags.intersects(Flags::DIM) {
342 fg.a *= 0.7;
343 }
344
345 let underline = (flags.intersects(Flags::ALL_UNDERLINES)
346 || indexed.cell.hyperlink().is_some())
347 .then(|| UnderlineStyle {
348 color: Some(fg),
349 thickness: Pixels::from(1.0),
350 wavy: flags.contains(Flags::UNDERCURL),
351 });
352
353 let strikethrough = flags
354 .intersects(Flags::STRIKEOUT)
355 .then(|| StrikethroughStyle {
356 color: Some(fg),
357 thickness: Pixels::from(1.0),
358 });
359
360 let weight = if flags.intersects(Flags::BOLD) {
361 FontWeight::BOLD
362 } else {
363 FontWeight::NORMAL
364 };
365
366 let style = if flags.intersects(Flags::ITALIC) {
367 FontStyle::Italic
368 } else {
369 FontStyle::Normal
370 };
371
372 let mut result = TextRun {
373 len: indexed.c.len_utf8(),
374 color: fg,
375 background_color: None,
376 font: Font {
377 weight,
378 style,
379 ..text_style.font()
380 },
381 underline,
382 strikethrough,
383 };
384
385 if let Some((style, range)) = hyperlink {
386 if range.contains(&indexed.point) {
387 if let Some(underline) = style.underline {
388 result.underline = Some(underline);
389 }
390
391 if let Some(color) = style.color {
392 result.color = color;
393 }
394 }
395 }
396
397 result
398 }
399
400 fn generic_button_handler<E>(
401 connection: Model<Terminal>,
402 origin: Point<Pixels>,
403 focus_handle: FocusHandle,
404 f: impl Fn(&mut Terminal, Point<Pixels>, &E, &mut ModelContext<Terminal>),
405 ) -> impl Fn(&E, &mut WindowContext) {
406 move |event, cx| {
407 cx.focus(&focus_handle);
408 connection.update(cx, |terminal, cx| {
409 f(terminal, origin, event, cx);
410
411 cx.notify();
412 })
413 }
414 }
415
416 fn register_mouse_listeners(
417 &mut self,
418 origin: Point<Pixels>,
419 mode: TermMode,
420 hitbox: &Hitbox,
421 cx: &mut WindowContext,
422 ) {
423 let focus = self.focus.clone();
424 let terminal = self.terminal.clone();
425
426 self.interactivity.on_mouse_down(MouseButton::Left, {
427 let terminal = terminal.clone();
428 let focus = focus.clone();
429 move |e, cx| {
430 cx.focus(&focus);
431 terminal.update(cx, |terminal, cx| {
432 terminal.mouse_down(&e, origin, cx);
433 cx.notify();
434 })
435 }
436 });
437
438 cx.on_mouse_event({
439 let focus = self.focus.clone();
440 let terminal = self.terminal.clone();
441 let hitbox = hitbox.clone();
442 move |e: &MouseMoveEvent, phase, cx| {
443 if phase != DispatchPhase::Bubble || !focus.is_focused(cx) {
444 return;
445 }
446
447 if e.pressed_button.is_some() && !cx.has_active_drag() {
448 let hovered = hitbox.is_hovered(cx);
449 terminal.update(cx, |terminal, cx| {
450 if terminal.selection_started() {
451 terminal.mouse_drag(e, origin, hitbox.bounds);
452 cx.notify();
453 } else {
454 if hovered {
455 terminal.mouse_drag(e, origin, hitbox.bounds);
456 cx.notify();
457 }
458 }
459 })
460 }
461
462 if hitbox.is_hovered(cx) {
463 terminal.update(cx, |terminal, cx| {
464 terminal.mouse_move(&e, origin);
465 cx.notify();
466 })
467 }
468 }
469 });
470
471 self.interactivity.on_mouse_up(
472 MouseButton::Left,
473 TerminalElement::generic_button_handler(
474 terminal.clone(),
475 origin,
476 focus.clone(),
477 move |terminal, origin, e, cx| {
478 terminal.mouse_up(&e, origin, cx);
479 },
480 ),
481 );
482 self.interactivity.on_mouse_down(
483 MouseButton::Middle,
484 TerminalElement::generic_button_handler(
485 terminal.clone(),
486 origin,
487 focus.clone(),
488 move |terminal, origin, e, cx| {
489 terminal.mouse_down(&e, origin, cx);
490 },
491 ),
492 );
493 self.interactivity.on_scroll_wheel({
494 let terminal = terminal.clone();
495 move |e, cx| {
496 terminal.update(cx, |terminal, cx| {
497 terminal.scroll_wheel(e, origin);
498 cx.notify();
499 })
500 }
501 });
502
503 // Mouse mode handlers:
504 // All mouse modes need the extra click handlers
505 if mode.intersects(TermMode::MOUSE_MODE) {
506 self.interactivity.on_mouse_down(
507 MouseButton::Right,
508 TerminalElement::generic_button_handler(
509 terminal.clone(),
510 origin,
511 focus.clone(),
512 move |terminal, origin, e, cx| {
513 terminal.mouse_down(&e, origin, cx);
514 },
515 ),
516 );
517 self.interactivity.on_mouse_up(
518 MouseButton::Right,
519 TerminalElement::generic_button_handler(
520 terminal.clone(),
521 origin,
522 focus.clone(),
523 move |terminal, origin, e, cx| {
524 terminal.mouse_up(&e, origin, cx);
525 },
526 ),
527 );
528 self.interactivity.on_mouse_up(
529 MouseButton::Middle,
530 TerminalElement::generic_button_handler(
531 terminal,
532 origin,
533 focus,
534 move |terminal, origin, e, cx| {
535 terminal.mouse_up(&e, origin, cx);
536 },
537 ),
538 );
539 }
540 }
541}
542
543impl Element for TerminalElement {
544 type RequestLayoutState = ();
545 type PrepaintState = LayoutState;
546
547 fn id(&self) -> Option<ElementId> {
548 self.interactivity.element_id.clone()
549 }
550
551 fn request_layout(
552 &mut self,
553 global_id: Option<&GlobalElementId>,
554 cx: &mut WindowContext,
555 ) -> (LayoutId, Self::RequestLayoutState) {
556 self.interactivity.occlude_mouse();
557 let layout_id = self
558 .interactivity
559 .request_layout(global_id, cx, |mut style, cx| {
560 style.size.width = relative(1.).into();
561 style.size.height = relative(1.).into();
562 let layout_id = cx.request_layout(style, None);
563
564 layout_id
565 });
566 (layout_id, ())
567 }
568
569 fn prepaint(
570 &mut self,
571 global_id: Option<&GlobalElementId>,
572 bounds: Bounds<Pixels>,
573 _: &mut Self::RequestLayoutState,
574 cx: &mut WindowContext,
575 ) -> Self::PrepaintState {
576 self.interactivity
577 .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| {
578 let hitbox = hitbox.unwrap();
579 let settings = ThemeSettings::get_global(cx).clone();
580
581 let buffer_font_size = settings.buffer_font_size(cx);
582
583 let terminal_settings = TerminalSettings::get_global(cx);
584 let font_family = terminal_settings
585 .font_family
586 .as_ref()
587 .map(|string| string.clone().into())
588 .unwrap_or(settings.buffer_font.family);
589
590 let font_features = terminal_settings
591 .font_features
592 .clone()
593 .unwrap_or(settings.buffer_font.features.clone());
594
595 let font_weight = terminal_settings.font_weight.unwrap_or_default();
596
597 let line_height = terminal_settings.line_height.value();
598 let font_size = terminal_settings.font_size;
599
600 let font_size =
601 font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx));
602
603 let theme = cx.theme().clone();
604
605 let link_style = HighlightStyle {
606 color: Some(theme.colors().link_text_hover),
607 font_weight: None,
608 font_style: None,
609 background_color: None,
610 underline: Some(UnderlineStyle {
611 thickness: px(1.0),
612 color: Some(theme.colors().link_text_hover),
613 wavy: false,
614 }),
615 strikethrough: None,
616 fade_out: None,
617 };
618
619 let text_style = TextStyle {
620 font_family,
621 font_features,
622 font_weight,
623 font_size: font_size.into(),
624 font_style: FontStyle::Normal,
625 line_height: line_height.into(),
626 background_color: None,
627 white_space: WhiteSpace::Normal,
628 // These are going to be overridden per-cell
629 underline: None,
630 strikethrough: None,
631 color: theme.colors().text,
632 };
633
634 let text_system = cx.text_system();
635 let player_color = theme.players().local();
636 let match_color = theme.colors().search_match_background;
637 let gutter;
638 let dimensions = {
639 let rem_size = cx.rem_size();
640 let font_pixels = text_style.font_size.to_pixels(rem_size);
641 let line_height = font_pixels * line_height.to_pixels(rem_size);
642 let font_id = cx.text_system().resolve_font(&text_style.font());
643
644 let cell_width = text_system
645 .advance(font_id, font_pixels, 'm')
646 .unwrap()
647 .width;
648 gutter = cell_width;
649
650 let mut size = bounds.size;
651 size.width -= gutter;
652
653 // https://github.com/zed-industries/zed/issues/2750
654 // if the terminal is one column wide, rendering 🦀
655 // causes alacritty to misbehave.
656 if size.width < cell_width * 2.0 {
657 size.width = cell_width * 2.0;
658 }
659
660 TerminalSize::new(line_height, cell_width, size)
661 };
662
663 let search_matches = self.terminal.read(cx).matches.clone();
664
665 let background_color = theme.colors().terminal_background;
666
667 let last_hovered_word = self.terminal.update(cx, |terminal, cx| {
668 terminal.set_size(dimensions);
669 terminal.sync(cx);
670 if self.can_navigate_to_selected_word
671 && terminal.can_navigate_to_selected_word()
672 {
673 terminal.last_content.last_hovered_word.clone()
674 } else {
675 None
676 }
677 });
678
679 let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
680 let offset = bounds.origin + Point::new(gutter, px(0.));
681 let mut element = div()
682 .size_full()
683 .id("terminal-element")
684 .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx))
685 .into_any_element();
686 element.prepaint_as_root(offset, bounds.size.into(), cx);
687 element
688 });
689
690 let TerminalContent {
691 cells,
692 mode,
693 display_offset,
694 cursor_char,
695 selection,
696 cursor,
697 ..
698 } = &self.terminal.read(cx).last_content;
699
700 // searches, highlights to a single range representations
701 let mut relative_highlighted_ranges = Vec::new();
702 for search_match in search_matches {
703 relative_highlighted_ranges.push((search_match, match_color))
704 }
705 if let Some(selection) = selection {
706 relative_highlighted_ranges
707 .push((selection.start..=selection.end, player_color.selection));
708 }
709
710 // then have that representation be converted to the appropriate highlight data structure
711
712 let (cells, rects) = TerminalElement::layout_grid(
713 cells,
714 &text_style,
715 &cx.text_system(),
716 last_hovered_word
717 .as_ref()
718 .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
719 cx,
720 );
721
722 // Layout cursor. Rectangle is used for IME, so we should lay it out even
723 // if we don't end up showing it.
724 let cursor = if let AlacCursorShape::Hidden = cursor.shape {
725 None
726 } else {
727 let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
728 let cursor_text = {
729 let str_trxt = cursor_char.to_string();
730 let len = str_trxt.len();
731 cx.text_system()
732 .shape_line(
733 str_trxt.into(),
734 text_style.font_size.to_pixels(cx.rem_size()),
735 &[TextRun {
736 len,
737 font: text_style.font(),
738 color: theme.colors().terminal_background,
739 background_color: None,
740 underline: Default::default(),
741 strikethrough: None,
742 }],
743 )
744 .unwrap()
745 };
746
747 let focused = self.focused;
748 TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
749 move |(cursor_position, block_width)| {
750 let (shape, text) = match cursor.shape {
751 AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
752 AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
753 AlacCursorShape::Underline => (CursorShape::Underscore, None),
754 AlacCursorShape::Beam => (CursorShape::Bar, None),
755 AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
756 //This case is handled in the if wrapping the whole cursor layout
757 AlacCursorShape::Hidden => unreachable!(),
758 };
759
760 CursorLayout::new(
761 cursor_position,
762 block_width,
763 dimensions.line_height,
764 theme.players().local().cursor,
765 shape,
766 text,
767 )
768 },
769 )
770 };
771
772 LayoutState {
773 hitbox,
774 cells,
775 cursor,
776 background_color,
777 dimensions,
778 rects,
779 relative_highlighted_ranges,
780 mode: *mode,
781 display_offset: *display_offset,
782 hyperlink_tooltip,
783 gutter,
784 last_hovered_word,
785 }
786 })
787 }
788
789 fn paint(
790 &mut self,
791 global_id: Option<&GlobalElementId>,
792 bounds: Bounds<Pixels>,
793 _: &mut Self::RequestLayoutState,
794 layout: &mut Self::PrepaintState,
795 cx: &mut WindowContext<'_>,
796 ) {
797 cx.paint_quad(fill(bounds, layout.background_color));
798 let origin = bounds.origin + Point::new(layout.gutter, px(0.));
799
800 let terminal_input_handler = TerminalInputHandler {
801 terminal: self.terminal.clone(),
802 cursor_bounds: layout
803 .cursor
804 .as_ref()
805 .map(|cursor| cursor.bounding_rect(origin)),
806 workspace: self.workspace.clone(),
807 };
808
809 self.register_mouse_listeners(origin, layout.mode, &layout.hitbox, cx);
810 if self.can_navigate_to_selected_word && layout.last_hovered_word.is_some() {
811 cx.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
812 } else {
813 cx.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
814 }
815
816 let cursor = layout.cursor.take();
817 let hyperlink_tooltip = layout.hyperlink_tooltip.take();
818 self.interactivity
819 .paint(global_id, bounds, Some(&layout.hitbox), cx, |_, cx| {
820 cx.handle_input(&self.focus, terminal_input_handler);
821
822 cx.on_key_event({
823 let this = self.terminal.clone();
824 move |event: &ModifiersChangedEvent, phase, cx| {
825 if phase != DispatchPhase::Bubble {
826 return;
827 }
828
829 let handled =
830 this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
831
832 if handled {
833 cx.refresh();
834 }
835 }
836 });
837
838 for rect in &layout.rects {
839 rect.paint(origin, &layout, cx);
840 }
841
842 for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
843 {
844 if let Some((start_y, highlighted_range_lines)) =
845 to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
846 {
847 let hr = HighlightedRange {
848 start_y, //Need to change this
849 line_height: layout.dimensions.line_height,
850 lines: highlighted_range_lines,
851 color: *color,
852 //Copied from editor. TODO: move to theme or something
853 corner_radius: 0.15 * layout.dimensions.line_height,
854 };
855 hr.paint(bounds, cx);
856 }
857 }
858
859 for cell in &layout.cells {
860 cell.paint(origin, &layout, bounds, cx);
861 }
862
863 if self.cursor_visible {
864 if let Some(mut cursor) = cursor {
865 cursor.paint(origin, cx);
866 }
867 }
868
869 if let Some(mut element) = hyperlink_tooltip {
870 element.paint(cx);
871 }
872 });
873 }
874}
875
876impl IntoElement for TerminalElement {
877 type Element = Self;
878
879 fn into_element(self) -> Self::Element {
880 self
881 }
882}
883
884struct TerminalInputHandler {
885 terminal: Model<Terminal>,
886 workspace: WeakView<Workspace>,
887 cursor_bounds: Option<Bounds<Pixels>>,
888}
889
890impl InputHandler for TerminalInputHandler {
891 fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<std::ops::Range<usize>> {
892 if self
893 .terminal
894 .read(cx)
895 .last_content
896 .mode
897 .contains(TermMode::ALT_SCREEN)
898 {
899 None
900 } else {
901 Some(0..0)
902 }
903 }
904
905 fn marked_text_range(&mut self, _: &mut WindowContext) -> Option<std::ops::Range<usize>> {
906 None
907 }
908
909 fn text_for_range(
910 &mut self,
911 _: std::ops::Range<usize>,
912 _: &mut WindowContext,
913 ) -> Option<String> {
914 None
915 }
916
917 fn replace_text_in_range(
918 &mut self,
919 _replacement_range: Option<std::ops::Range<usize>>,
920 text: &str,
921 cx: &mut WindowContext,
922 ) {
923 self.terminal.update(cx, |terminal, _| {
924 terminal.input(text.into());
925 });
926
927 self.workspace
928 .update(cx, |this, cx| {
929 let telemetry = this.project().read(cx).client().telemetry().clone();
930 telemetry.log_edit_event("terminal");
931 })
932 .ok();
933 }
934
935 fn replace_and_mark_text_in_range(
936 &mut self,
937 _range_utf16: Option<std::ops::Range<usize>>,
938 _new_text: &str,
939 _new_selected_range: Option<std::ops::Range<usize>>,
940 _: &mut WindowContext,
941 ) {
942 }
943
944 fn unmark_text(&mut self, _: &mut WindowContext) {}
945
946 fn bounds_for_range(
947 &mut self,
948 _range_utf16: std::ops::Range<usize>,
949 _: &mut WindowContext,
950 ) -> Option<Bounds<Pixels>> {
951 self.cursor_bounds
952 }
953}
954
955fn is_blank(cell: &IndexedCell) -> bool {
956 if cell.c != ' ' {
957 return false;
958 }
959
960 if cell.bg != AnsiColor::Named(NamedColor::Background) {
961 return false;
962 }
963
964 if cell.hyperlink().is_some() {
965 return false;
966 }
967
968 if cell
969 .flags
970 .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
971 {
972 return false;
973 }
974
975 return true;
976}
977
978fn to_highlighted_range_lines(
979 range: &RangeInclusive<AlacPoint>,
980 layout: &LayoutState,
981 origin: Point<Pixels>,
982) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
983 // Step 1. Normalize the points to be viewport relative.
984 // When display_offset = 1, here's how the grid is arranged:
985 //-2,0 -2,1...
986 //--- Viewport top
987 //-1,0 -1,1...
988 //--------- Terminal Top
989 // 0,0 0,1...
990 // 1,0 1,1...
991 //--- Viewport Bottom
992 // 2,0 2,1...
993 //--------- Terminal Bottom
994
995 // Normalize to viewport relative, from terminal relative.
996 // lines are i32s, which are negative above the top left corner of the terminal
997 // If the user has scrolled, we use the display_offset to tell us which offset
998 // of the grid data we should be looking at. But for the rendering step, we don't
999 // want negatives. We want things relative to the 'viewport' (the area of the grid
1000 // which is currently shown according to the display offset)
1001 let unclamped_start = AlacPoint::new(
1002 range.start().line + layout.display_offset,
1003 range.start().column,
1004 );
1005 let unclamped_end =
1006 AlacPoint::new(range.end().line + layout.display_offset, range.end().column);
1007
1008 // Step 2. Clamp range to viewport, and return None if it doesn't overlap
1009 if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 {
1010 return None;
1011 }
1012
1013 let clamped_start_line = unclamped_start.line.0.max(0) as usize;
1014 let clamped_end_line = unclamped_end
1015 .line
1016 .0
1017 .min(layout.dimensions.num_lines() as i32) as usize;
1018 //Convert the start of the range to pixels
1019 let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height;
1020
1021 // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
1022 // (also convert to pixels)
1023 let mut highlighted_range_lines = Vec::new();
1024 for line in clamped_start_line..=clamped_end_line {
1025 let mut line_start = 0;
1026 let mut line_end = layout.dimensions.columns();
1027
1028 if line == clamped_start_line {
1029 line_start = unclamped_start.column.0;
1030 }
1031 if line == clamped_end_line {
1032 line_end = unclamped_end.column.0 + 1; // +1 for inclusive
1033 }
1034
1035 highlighted_range_lines.push(HighlightedRangeLine {
1036 start_x: origin.x + line_start as f32 * layout.dimensions.cell_width,
1037 end_x: origin.x + line_end as f32 * layout.dimensions.cell_width,
1038 });
1039 }
1040
1041 Some((start_y, highlighted_range_lines))
1042}
1043
1044/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent.
1045fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
1046 let colors = theme.colors();
1047 match fg {
1048 // Named and theme defined colors
1049 terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n {
1050 NamedColor::Black => colors.terminal_ansi_black,
1051 NamedColor::Red => colors.terminal_ansi_red,
1052 NamedColor::Green => colors.terminal_ansi_green,
1053 NamedColor::Yellow => colors.terminal_ansi_yellow,
1054 NamedColor::Blue => colors.terminal_ansi_blue,
1055 NamedColor::Magenta => colors.terminal_ansi_magenta,
1056 NamedColor::Cyan => colors.terminal_ansi_cyan,
1057 NamedColor::White => colors.terminal_ansi_white,
1058 NamedColor::BrightBlack => colors.terminal_ansi_bright_black,
1059 NamedColor::BrightRed => colors.terminal_ansi_bright_red,
1060 NamedColor::BrightGreen => colors.terminal_ansi_bright_green,
1061 NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow,
1062 NamedColor::BrightBlue => colors.terminal_ansi_bright_blue,
1063 NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta,
1064 NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan,
1065 NamedColor::BrightWhite => colors.terminal_ansi_bright_white,
1066 NamedColor::Foreground => colors.text,
1067 NamedColor::Background => colors.background,
1068 NamedColor::Cursor => theme.players().local().cursor,
1069 NamedColor::DimBlack => colors.terminal_ansi_dim_black,
1070 NamedColor::DimRed => colors.terminal_ansi_dim_red,
1071 NamedColor::DimGreen => colors.terminal_ansi_dim_green,
1072 NamedColor::DimYellow => colors.terminal_ansi_dim_yellow,
1073 NamedColor::DimBlue => colors.terminal_ansi_dim_blue,
1074 NamedColor::DimMagenta => colors.terminal_ansi_dim_magenta,
1075 NamedColor::DimCyan => colors.terminal_ansi_dim_cyan,
1076 NamedColor::DimWhite => colors.terminal_ansi_dim_white,
1077 NamedColor::BrightForeground => colors.terminal_bright_foreground,
1078 NamedColor::DimForeground => colors.terminal_dim_foreground,
1079 },
1080 // 'True' colors
1081 terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => {
1082 terminal::rgba_color(rgb.r, rgb.g, rgb.b)
1083 }
1084 // 8 bit, indexed colors
1085 terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => {
1086 terminal::get_color_at_index(*i as usize, theme)
1087 }
1088 }
1089}