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