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