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 line_height = terminal_settings.line_height.value();
596 let font_size = terminal_settings.font_size;
597
598 let font_size =
599 font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx));
600
601 let theme = cx.theme().clone();
602
603 let link_style = HighlightStyle {
604 color: Some(theme.colors().link_text_hover),
605 font_weight: None,
606 font_style: None,
607 background_color: None,
608 underline: Some(UnderlineStyle {
609 thickness: px(1.0),
610 color: Some(theme.colors().link_text_hover),
611 wavy: false,
612 }),
613 strikethrough: None,
614 fade_out: None,
615 };
616
617 let text_style = TextStyle {
618 font_family,
619 font_features,
620 font_size: font_size.into(),
621 font_style: FontStyle::Normal,
622 line_height: line_height.into(),
623 background_color: None,
624 white_space: WhiteSpace::Normal,
625 // These are going to be overridden per-cell
626 underline: None,
627 strikethrough: None,
628 color: theme.colors().text,
629 font_weight: FontWeight::NORMAL,
630 };
631
632 let text_system = cx.text_system();
633 let player_color = theme.players().local();
634 let match_color = theme.colors().search_match_background;
635 let gutter;
636 let dimensions = {
637 let rem_size = cx.rem_size();
638 let font_pixels = text_style.font_size.to_pixels(rem_size);
639 let line_height = font_pixels * line_height.to_pixels(rem_size);
640 let font_id = cx.text_system().resolve_font(&text_style.font());
641
642 let cell_width = text_system
643 .advance(font_id, font_pixels, 'm')
644 .unwrap()
645 .width;
646 gutter = cell_width;
647
648 let mut size = bounds.size;
649 size.width -= gutter;
650
651 // https://github.com/zed-industries/zed/issues/2750
652 // if the terminal is one column wide, rendering 🦀
653 // causes alacritty to misbehave.
654 if size.width < cell_width * 2.0 {
655 size.width = cell_width * 2.0;
656 }
657
658 TerminalSize::new(line_height, cell_width, size)
659 };
660
661 let search_matches = self.terminal.read(cx).matches.clone();
662
663 let background_color = theme.colors().terminal_background;
664
665 let last_hovered_word = self.terminal.update(cx, |terminal, cx| {
666 terminal.set_size(dimensions);
667 terminal.sync(cx);
668 if self.can_navigate_to_selected_word
669 && terminal.can_navigate_to_selected_word()
670 {
671 terminal.last_content.last_hovered_word.clone()
672 } else {
673 None
674 }
675 });
676
677 let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
678 let offset = bounds.origin + Point::new(gutter, px(0.));
679 let mut element = div()
680 .size_full()
681 .id("terminal-element")
682 .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx))
683 .into_any_element();
684 element.prepaint_as_root(offset, bounds.size.into(), cx);
685 element
686 });
687
688 let TerminalContent {
689 cells,
690 mode,
691 display_offset,
692 cursor_char,
693 selection,
694 cursor,
695 ..
696 } = &self.terminal.read(cx).last_content;
697
698 // searches, highlights to a single range representations
699 let mut relative_highlighted_ranges = Vec::new();
700 for search_match in search_matches {
701 relative_highlighted_ranges.push((search_match, match_color))
702 }
703 if let Some(selection) = selection {
704 relative_highlighted_ranges
705 .push((selection.start..=selection.end, player_color.selection));
706 }
707
708 // then have that representation be converted to the appropriate highlight data structure
709
710 let (cells, rects) = TerminalElement::layout_grid(
711 cells,
712 &text_style,
713 &cx.text_system(),
714 last_hovered_word
715 .as_ref()
716 .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
717 cx,
718 );
719
720 // Layout cursor. Rectangle is used for IME, so we should lay it out even
721 // if we don't end up showing it.
722 let cursor = if let AlacCursorShape::Hidden = cursor.shape {
723 None
724 } else {
725 let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
726 let cursor_text = {
727 let str_trxt = cursor_char.to_string();
728 let len = str_trxt.len();
729 cx.text_system()
730 .shape_line(
731 str_trxt.into(),
732 text_style.font_size.to_pixels(cx.rem_size()),
733 &[TextRun {
734 len,
735 font: text_style.font(),
736 color: theme.colors().terminal_background,
737 background_color: None,
738 underline: Default::default(),
739 strikethrough: None,
740 }],
741 )
742 .unwrap()
743 };
744
745 let focused = self.focused;
746 TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
747 move |(cursor_position, block_width)| {
748 let (shape, text) = match cursor.shape {
749 AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
750 AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
751 AlacCursorShape::Underline => (CursorShape::Underscore, None),
752 AlacCursorShape::Beam => (CursorShape::Bar, None),
753 AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
754 //This case is handled in the if wrapping the whole cursor layout
755 AlacCursorShape::Hidden => unreachable!(),
756 };
757
758 CursorLayout::new(
759 cursor_position,
760 block_width,
761 dimensions.line_height,
762 theme.players().local().cursor,
763 shape,
764 text,
765 )
766 },
767 )
768 };
769
770 LayoutState {
771 hitbox,
772 cells,
773 cursor,
774 background_color,
775 dimensions,
776 rects,
777 relative_highlighted_ranges,
778 mode: *mode,
779 display_offset: *display_offset,
780 hyperlink_tooltip,
781 gutter,
782 last_hovered_word,
783 }
784 })
785 }
786
787 fn paint(
788 &mut self,
789 global_id: Option<&GlobalElementId>,
790 bounds: Bounds<Pixels>,
791 _: &mut Self::RequestLayoutState,
792 layout: &mut Self::PrepaintState,
793 cx: &mut WindowContext<'_>,
794 ) {
795 cx.paint_quad(fill(bounds, layout.background_color));
796 let origin = bounds.origin + Point::new(layout.gutter, px(0.));
797
798 let terminal_input_handler = TerminalInputHandler {
799 terminal: self.terminal.clone(),
800 cursor_bounds: layout
801 .cursor
802 .as_ref()
803 .map(|cursor| cursor.bounding_rect(origin)),
804 workspace: self.workspace.clone(),
805 };
806
807 self.register_mouse_listeners(origin, layout.mode, &layout.hitbox, cx);
808 if self.can_navigate_to_selected_word && layout.last_hovered_word.is_some() {
809 cx.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
810 } else {
811 cx.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
812 }
813
814 let cursor = layout.cursor.take();
815 let hyperlink_tooltip = layout.hyperlink_tooltip.take();
816 self.interactivity
817 .paint(global_id, bounds, Some(&layout.hitbox), cx, |_, cx| {
818 cx.handle_input(&self.focus, terminal_input_handler);
819
820 cx.on_key_event({
821 let this = self.terminal.clone();
822 move |event: &ModifiersChangedEvent, phase, cx| {
823 if phase != DispatchPhase::Bubble {
824 return;
825 }
826
827 let handled =
828 this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
829
830 if handled {
831 cx.refresh();
832 }
833 }
834 });
835
836 for rect in &layout.rects {
837 rect.paint(origin, &layout, cx);
838 }
839
840 for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
841 {
842 if let Some((start_y, highlighted_range_lines)) =
843 to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
844 {
845 let hr = HighlightedRange {
846 start_y, //Need to change this
847 line_height: layout.dimensions.line_height,
848 lines: highlighted_range_lines,
849 color: *color,
850 //Copied from editor. TODO: move to theme or something
851 corner_radius: 0.15 * layout.dimensions.line_height,
852 };
853 hr.paint(bounds, cx);
854 }
855 }
856
857 for cell in &layout.cells {
858 cell.paint(origin, &layout, bounds, cx);
859 }
860
861 if self.cursor_visible {
862 if let Some(mut cursor) = cursor {
863 cursor.paint(origin, cx);
864 }
865 }
866
867 if let Some(mut element) = hyperlink_tooltip {
868 element.paint(cx);
869 }
870 });
871 }
872}
873
874impl IntoElement for TerminalElement {
875 type Element = Self;
876
877 fn into_element(self) -> Self::Element {
878 self
879 }
880}
881
882struct TerminalInputHandler {
883 terminal: Model<Terminal>,
884 workspace: WeakView<Workspace>,
885 cursor_bounds: Option<Bounds<Pixels>>,
886}
887
888impl InputHandler for TerminalInputHandler {
889 fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<std::ops::Range<usize>> {
890 if self
891 .terminal
892 .read(cx)
893 .last_content
894 .mode
895 .contains(TermMode::ALT_SCREEN)
896 {
897 None
898 } else {
899 Some(0..0)
900 }
901 }
902
903 fn marked_text_range(&mut self, _: &mut WindowContext) -> Option<std::ops::Range<usize>> {
904 None
905 }
906
907 fn text_for_range(
908 &mut self,
909 _: std::ops::Range<usize>,
910 _: &mut WindowContext,
911 ) -> Option<String> {
912 None
913 }
914
915 fn replace_text_in_range(
916 &mut self,
917 _replacement_range: Option<std::ops::Range<usize>>,
918 text: &str,
919 cx: &mut WindowContext,
920 ) {
921 self.terminal.update(cx, |terminal, _| {
922 terminal.input(text.into());
923 });
924
925 self.workspace
926 .update(cx, |this, cx| {
927 let telemetry = this.project().read(cx).client().telemetry().clone();
928 telemetry.log_edit_event("terminal");
929 })
930 .ok();
931 }
932
933 fn replace_and_mark_text_in_range(
934 &mut self,
935 _range_utf16: Option<std::ops::Range<usize>>,
936 _new_text: &str,
937 _new_selected_range: Option<std::ops::Range<usize>>,
938 _: &mut WindowContext,
939 ) {
940 }
941
942 fn unmark_text(&mut self, _: &mut WindowContext) {}
943
944 fn bounds_for_range(
945 &mut self,
946 _range_utf16: std::ops::Range<usize>,
947 _: &mut WindowContext,
948 ) -> Option<Bounds<Pixels>> {
949 self.cursor_bounds
950 }
951}
952
953fn is_blank(cell: &IndexedCell) -> bool {
954 if cell.c != ' ' {
955 return false;
956 }
957
958 if cell.bg != AnsiColor::Named(NamedColor::Background) {
959 return false;
960 }
961
962 if cell.hyperlink().is_some() {
963 return false;
964 }
965
966 if cell
967 .flags
968 .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
969 {
970 return false;
971 }
972
973 return true;
974}
975
976fn to_highlighted_range_lines(
977 range: &RangeInclusive<AlacPoint>,
978 layout: &LayoutState,
979 origin: Point<Pixels>,
980) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
981 // Step 1. Normalize the points to be viewport relative.
982 // When display_offset = 1, here's how the grid is arranged:
983 //-2,0 -2,1...
984 //--- Viewport top
985 //-1,0 -1,1...
986 //--------- Terminal Top
987 // 0,0 0,1...
988 // 1,0 1,1...
989 //--- Viewport Bottom
990 // 2,0 2,1...
991 //--------- Terminal Bottom
992
993 // Normalize to viewport relative, from terminal relative.
994 // lines are i32s, which are negative above the top left corner of the terminal
995 // If the user has scrolled, we use the display_offset to tell us which offset
996 // of the grid data we should be looking at. But for the rendering step, we don't
997 // want negatives. We want things relative to the 'viewport' (the area of the grid
998 // which is currently shown according to the display offset)
999 let unclamped_start = AlacPoint::new(
1000 range.start().line + layout.display_offset,
1001 range.start().column,
1002 );
1003 let unclamped_end =
1004 AlacPoint::new(range.end().line + layout.display_offset, range.end().column);
1005
1006 // Step 2. Clamp range to viewport, and return None if it doesn't overlap
1007 if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 {
1008 return None;
1009 }
1010
1011 let clamped_start_line = unclamped_start.line.0.max(0) as usize;
1012 let clamped_end_line = unclamped_end
1013 .line
1014 .0
1015 .min(layout.dimensions.num_lines() as i32) as usize;
1016 //Convert the start of the range to pixels
1017 let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height;
1018
1019 // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
1020 // (also convert to pixels)
1021 let mut highlighted_range_lines = Vec::new();
1022 for line in clamped_start_line..=clamped_end_line {
1023 let mut line_start = 0;
1024 let mut line_end = layout.dimensions.columns();
1025
1026 if line == clamped_start_line {
1027 line_start = unclamped_start.column.0;
1028 }
1029 if line == clamped_end_line {
1030 line_end = unclamped_end.column.0 + 1; // +1 for inclusive
1031 }
1032
1033 highlighted_range_lines.push(HighlightedRangeLine {
1034 start_x: origin.x + line_start as f32 * layout.dimensions.cell_width,
1035 end_x: origin.x + line_end as f32 * layout.dimensions.cell_width,
1036 });
1037 }
1038
1039 Some((start_y, highlighted_range_lines))
1040}
1041
1042/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent.
1043fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
1044 let colors = theme.colors();
1045 match fg {
1046 // Named and theme defined colors
1047 terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n {
1048 NamedColor::Black => colors.terminal_ansi_black,
1049 NamedColor::Red => colors.terminal_ansi_red,
1050 NamedColor::Green => colors.terminal_ansi_green,
1051 NamedColor::Yellow => colors.terminal_ansi_yellow,
1052 NamedColor::Blue => colors.terminal_ansi_blue,
1053 NamedColor::Magenta => colors.terminal_ansi_magenta,
1054 NamedColor::Cyan => colors.terminal_ansi_cyan,
1055 NamedColor::White => colors.terminal_ansi_white,
1056 NamedColor::BrightBlack => colors.terminal_ansi_bright_black,
1057 NamedColor::BrightRed => colors.terminal_ansi_bright_red,
1058 NamedColor::BrightGreen => colors.terminal_ansi_bright_green,
1059 NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow,
1060 NamedColor::BrightBlue => colors.terminal_ansi_bright_blue,
1061 NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta,
1062 NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan,
1063 NamedColor::BrightWhite => colors.terminal_ansi_bright_white,
1064 NamedColor::Foreground => colors.text,
1065 NamedColor::Background => colors.background,
1066 NamedColor::Cursor => theme.players().local().cursor,
1067 NamedColor::DimBlack => colors.terminal_ansi_dim_black,
1068 NamedColor::DimRed => colors.terminal_ansi_dim_red,
1069 NamedColor::DimGreen => colors.terminal_ansi_dim_green,
1070 NamedColor::DimYellow => colors.terminal_ansi_dim_yellow,
1071 NamedColor::DimBlue => colors.terminal_ansi_dim_blue,
1072 NamedColor::DimMagenta => colors.terminal_ansi_dim_magenta,
1073 NamedColor::DimCyan => colors.terminal_ansi_dim_cyan,
1074 NamedColor::DimWhite => colors.terminal_ansi_dim_white,
1075 NamedColor::BrightForeground => colors.terminal_bright_foreground,
1076 NamedColor::DimForeground => colors.terminal_dim_foreground,
1077 },
1078 // 'True' colors
1079 terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => {
1080 terminal::rgba_color(rgb.r, rgb.g, rgb.b)
1081 }
1082 // 8 bit, indexed colors
1083 terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => {
1084 terminal::get_color_at_index(*i as usize, theme)
1085 }
1086 }
1087}