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