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.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 None,
554 )
555 },
556 )
557 };
558
559 //Done!
560 LayoutState {
561 cells,
562 cursor,
563 background_color,
564 dimensions,
565 rects,
566 relative_highlighted_ranges,
567 mode: *mode,
568 display_offset: *display_offset,
569 hyperlink_tooltip,
570 gutter,
571 }
572 }
573
574 fn generic_button_handler<E>(
575 connection: Model<Terminal>,
576 origin: Point<Pixels>,
577 focus_handle: FocusHandle,
578 f: impl Fn(&mut Terminal, Point<Pixels>, &E, &mut ModelContext<Terminal>),
579 ) -> impl Fn(&E, &mut WindowContext) {
580 move |event, cx| {
581 cx.focus(&focus_handle);
582 connection.update(cx, |terminal, cx| {
583 f(terminal, origin, event, cx);
584
585 cx.notify();
586 })
587 }
588 }
589
590 fn register_mouse_listeners(
591 &mut self,
592 origin: Point<Pixels>,
593 mode: TermMode,
594 bounds: Bounds<Pixels>,
595 cx: &mut WindowContext,
596 ) {
597 let focus = self.focus.clone();
598 let terminal = self.terminal.clone();
599 let interactive_bounds = InteractiveBounds {
600 bounds: bounds.intersect(&cx.content_mask().bounds),
601 stacking_order: cx.stacking_order().clone(),
602 };
603
604 self.interactivity.on_mouse_down(MouseButton::Left, {
605 let terminal = terminal.clone();
606 let focus = focus.clone();
607 move |e, cx| {
608 cx.focus(&focus);
609 terminal.update(cx, |terminal, cx| {
610 terminal.mouse_down(&e, origin);
611 cx.notify();
612 })
613 }
614 });
615
616 cx.on_mouse_event({
617 let bounds = bounds.clone();
618 let focus = self.focus.clone();
619 let terminal = self.terminal.clone();
620 move |e: &MouseMoveEvent, phase, cx| {
621 if phase != DispatchPhase::Bubble || !focus.is_focused(cx) {
622 return;
623 }
624
625 if e.pressed_button.is_some() && !cx.has_active_drag() {
626 terminal.update(cx, |terminal, cx| {
627 terminal.mouse_drag(e, origin, bounds);
628 cx.notify();
629 })
630 }
631
632 if interactive_bounds.visibly_contains(&e.position, cx) {
633 terminal.update(cx, |terminal, cx| {
634 terminal.mouse_move(&e, origin);
635 cx.notify();
636 })
637 }
638 }
639 });
640
641 self.interactivity.on_mouse_up(
642 MouseButton::Left,
643 TerminalElement::generic_button_handler(
644 terminal.clone(),
645 origin,
646 focus.clone(),
647 move |terminal, origin, e, cx| {
648 terminal.mouse_up(&e, origin, cx);
649 },
650 ),
651 );
652 self.interactivity.on_scroll_wheel({
653 let terminal = terminal.clone();
654 move |e, cx| {
655 terminal.update(cx, |terminal, cx| {
656 terminal.scroll_wheel(e, origin);
657 cx.notify();
658 })
659 }
660 });
661
662 // Mouse mode handlers:
663 // All mouse modes need the extra click handlers
664 if mode.intersects(TermMode::MOUSE_MODE) {
665 self.interactivity.on_mouse_down(
666 MouseButton::Right,
667 TerminalElement::generic_button_handler(
668 terminal.clone(),
669 origin,
670 focus.clone(),
671 move |terminal, origin, e, _cx| {
672 terminal.mouse_down(&e, origin);
673 },
674 ),
675 );
676 self.interactivity.on_mouse_down(
677 MouseButton::Middle,
678 TerminalElement::generic_button_handler(
679 terminal.clone(),
680 origin,
681 focus.clone(),
682 move |terminal, origin, e, _cx| {
683 terminal.mouse_down(&e, origin);
684 },
685 ),
686 );
687 self.interactivity.on_mouse_up(
688 MouseButton::Right,
689 TerminalElement::generic_button_handler(
690 terminal.clone(),
691 origin,
692 focus.clone(),
693 move |terminal, origin, e, cx| {
694 terminal.mouse_up(&e, origin, cx);
695 },
696 ),
697 );
698 self.interactivity.on_mouse_up(
699 MouseButton::Middle,
700 TerminalElement::generic_button_handler(
701 terminal,
702 origin,
703 focus,
704 move |terminal, origin, e, cx| {
705 terminal.mouse_up(&e, origin, cx);
706 },
707 ),
708 );
709 }
710 }
711}
712
713impl Element for TerminalElement {
714 type State = InteractiveElementState;
715
716 fn request_layout(
717 &mut self,
718 element_state: Option<Self::State>,
719 cx: &mut WindowContext<'_>,
720 ) -> (LayoutId, Self::State) {
721 let (layout_id, interactive_state) =
722 self.interactivity
723 .layout(element_state, cx, |mut style, cx| {
724 style.size.width = relative(1.).into();
725 style.size.height = relative(1.).into();
726 let layout_id = cx.request_layout(&style, None);
727
728 layout_id
729 });
730
731 (layout_id, interactive_state)
732 }
733
734 fn paint(
735 &mut self,
736 bounds: Bounds<Pixels>,
737 state: &mut Self::State,
738 cx: &mut WindowContext<'_>,
739 ) {
740 let mut layout = self.compute_layout(bounds, cx);
741
742 cx.paint_quad(fill(bounds, layout.background_color));
743 let origin = bounds.origin + Point::new(layout.gutter, px(0.));
744
745 let terminal_input_handler = TerminalInputHandler {
746 cx: cx.to_async(),
747 terminal: self.terminal.clone(),
748 cursor_bounds: layout
749 .cursor
750 .as_ref()
751 .map(|cursor| cursor.bounding_rect(origin)),
752 workspace: self.workspace.clone(),
753 };
754
755 self.register_mouse_listeners(origin, layout.mode, bounds, cx);
756
757 self.interactivity
758 .paint(bounds, bounds.size, state, cx, |_, _, cx| {
759 cx.handle_input(&self.focus, terminal_input_handler);
760
761 cx.on_key_event({
762 let this = self.terminal.clone();
763 move |event: &ModifiersChangedEvent, phase, cx| {
764 if phase != DispatchPhase::Bubble {
765 return;
766 }
767
768 let handled =
769 this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
770
771 if handled {
772 cx.refresh();
773 }
774 }
775 });
776
777 for rect in &layout.rects {
778 rect.paint(origin, &layout, cx);
779 }
780
781 cx.with_z_index(1, |cx| {
782 for (relative_highlighted_range, color) in
783 layout.relative_highlighted_ranges.iter()
784 {
785 if let Some((start_y, highlighted_range_lines)) =
786 to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
787 {
788 let hr = HighlightedRange {
789 start_y, //Need to change this
790 line_height: layout.dimensions.line_height,
791 lines: highlighted_range_lines,
792 color: color.clone(),
793 //Copied from editor. TODO: move to theme or something
794 corner_radius: 0.15 * layout.dimensions.line_height,
795 };
796 hr.paint(bounds, cx);
797 }
798 }
799 });
800
801 cx.with_z_index(2, |cx| {
802 for cell in &layout.cells {
803 cell.paint(origin, &layout, bounds, cx);
804 }
805 });
806
807 if self.cursor_visible {
808 cx.with_z_index(3, |cx| {
809 if let Some(cursor) = &layout.cursor {
810 cursor.paint(origin, cx);
811 }
812 });
813 }
814
815 if let Some(mut element) = layout.hyperlink_tooltip.take() {
816 element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx)
817 }
818 });
819 }
820}
821
822impl IntoElement for TerminalElement {
823 type Element = Self;
824
825 fn element_id(&self) -> Option<ElementId> {
826 Some("terminal".into())
827 }
828
829 fn into_element(self) -> Self::Element {
830 self
831 }
832}
833
834struct TerminalInputHandler {
835 cx: AsyncWindowContext,
836 terminal: Model<Terminal>,
837 workspace: WeakView<Workspace>,
838 cursor_bounds: Option<Bounds<Pixels>>,
839}
840
841impl PlatformInputHandler for TerminalInputHandler {
842 fn selected_text_range(&mut self) -> Option<std::ops::Range<usize>> {
843 self.cx
844 .update(|_, cx| {
845 if self
846 .terminal
847 .read(cx)
848 .last_content
849 .mode
850 .contains(TermMode::ALT_SCREEN)
851 {
852 None
853 } else {
854 Some(0..0)
855 }
856 })
857 .ok()
858 .flatten()
859 }
860
861 fn marked_text_range(&mut self) -> Option<std::ops::Range<usize>> {
862 None
863 }
864
865 fn text_for_range(&mut self, _: std::ops::Range<usize>) -> Option<String> {
866 None
867 }
868
869 fn replace_text_in_range(
870 &mut self,
871 _replacement_range: Option<std::ops::Range<usize>>,
872 text: &str,
873 ) {
874 self.cx
875 .update(|_, cx| {
876 self.terminal.update(cx, |terminal, _| {
877 terminal.input(text.into());
878 });
879
880 self.workspace
881 .update(cx, |this, cx| {
882 let telemetry = this.project().read(cx).client().telemetry().clone();
883 telemetry.log_edit_event("terminal");
884 })
885 .ok();
886 })
887 .ok();
888 }
889
890 fn replace_and_mark_text_in_range(
891 &mut self,
892 _range_utf16: Option<std::ops::Range<usize>>,
893 _new_text: &str,
894 _new_selected_range: Option<std::ops::Range<usize>>,
895 ) {
896 }
897
898 fn unmark_text(&mut self) {}
899
900 fn bounds_for_range(&mut self, _range_utf16: std::ops::Range<usize>) -> Option<Bounds<Pixels>> {
901 self.cursor_bounds
902 }
903}
904
905fn is_blank(cell: &IndexedCell) -> bool {
906 if cell.c != ' ' {
907 return false;
908 }
909
910 if cell.bg != AnsiColor::Named(NamedColor::Background) {
911 return false;
912 }
913
914 if cell.hyperlink().is_some() {
915 return false;
916 }
917
918 if cell
919 .flags
920 .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
921 {
922 return false;
923 }
924
925 return true;
926}
927
928fn to_highlighted_range_lines(
929 range: &RangeInclusive<AlacPoint>,
930 layout: &LayoutState,
931 origin: Point<Pixels>,
932) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
933 // Step 1. Normalize the points to be viewport relative.
934 // When display_offset = 1, here's how the grid is arranged:
935 //-2,0 -2,1...
936 //--- Viewport top
937 //-1,0 -1,1...
938 //--------- Terminal Top
939 // 0,0 0,1...
940 // 1,0 1,1...
941 //--- Viewport Bottom
942 // 2,0 2,1...
943 //--------- Terminal Bottom
944
945 // Normalize to viewport relative, from terminal relative.
946 // lines are i32s, which are negative above the top left corner of the terminal
947 // If the user has scrolled, we use the display_offset to tell us which offset
948 // of the grid data we should be looking at. But for the rendering step, we don't
949 // want negatives. We want things relative to the 'viewport' (the area of the grid
950 // which is currently shown according to the display offset)
951 let unclamped_start = AlacPoint::new(
952 range.start().line + layout.display_offset,
953 range.start().column,
954 );
955 let unclamped_end =
956 AlacPoint::new(range.end().line + layout.display_offset, range.end().column);
957
958 // Step 2. Clamp range to viewport, and return None if it doesn't overlap
959 if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 {
960 return None;
961 }
962
963 let clamped_start_line = unclamped_start.line.0.max(0) as usize;
964 let clamped_end_line = unclamped_end
965 .line
966 .0
967 .min(layout.dimensions.num_lines() as i32) as usize;
968 //Convert the start of the range to pixels
969 let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height;
970
971 // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
972 // (also convert to pixels)
973 let mut highlighted_range_lines = Vec::new();
974 for line in clamped_start_line..=clamped_end_line {
975 let mut line_start = 0;
976 let mut line_end = layout.dimensions.columns();
977
978 if line == clamped_start_line {
979 line_start = unclamped_start.column.0 as usize;
980 }
981 if line == clamped_end_line {
982 line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive
983 }
984
985 highlighted_range_lines.push(HighlightedRangeLine {
986 start_x: origin.x + line_start as f32 * layout.dimensions.cell_width,
987 end_x: origin.x + line_end as f32 * layout.dimensions.cell_width,
988 });
989 }
990
991 Some((start_y, highlighted_range_lines))
992}
993
994///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
995fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) -> Hsla {
996 let colors = theme.colors();
997 match fg {
998 //Named and theme defined colors
999 terminal::alacritty_terminal::ansi::Color::Named(n) => match n {
1000 NamedColor::Black => colors.terminal_ansi_black,
1001 NamedColor::Red => colors.terminal_ansi_red,
1002 NamedColor::Green => colors.terminal_ansi_green,
1003 NamedColor::Yellow => colors.terminal_ansi_yellow,
1004 NamedColor::Blue => colors.terminal_ansi_blue,
1005 NamedColor::Magenta => colors.terminal_ansi_magenta,
1006 NamedColor::Cyan => colors.terminal_ansi_cyan,
1007 NamedColor::White => colors.terminal_ansi_white,
1008 NamedColor::BrightBlack => colors.terminal_ansi_bright_black,
1009 NamedColor::BrightRed => colors.terminal_ansi_bright_red,
1010 NamedColor::BrightGreen => colors.terminal_ansi_bright_green,
1011 NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow,
1012 NamedColor::BrightBlue => colors.terminal_ansi_bright_blue,
1013 NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta,
1014 NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan,
1015 NamedColor::BrightWhite => colors.terminal_ansi_bright_white,
1016 NamedColor::Foreground => colors.text,
1017 NamedColor::Background => colors.background,
1018 NamedColor::Cursor => theme.players().local().cursor,
1019
1020 // todo!(more colors)
1021 NamedColor::DimBlack => red(),
1022 NamedColor::DimRed => red(),
1023 NamedColor::DimGreen => red(),
1024 NamedColor::DimYellow => red(),
1025 NamedColor::DimBlue => red(),
1026 NamedColor::DimMagenta => red(),
1027 NamedColor::DimCyan => red(),
1028 NamedColor::DimWhite => red(),
1029 NamedColor::BrightForeground => red(),
1030 NamedColor::DimForeground => red(),
1031 },
1032 //'True' colors
1033 terminal::alacritty_terminal::ansi::Color::Spec(rgb) => {
1034 terminal::rgba_color(rgb.r, rgb.g, rgb.b)
1035 }
1036 //8 bit, indexed colors
1037 terminal::alacritty_terminal::ansi::Color::Indexed(i) => {
1038 terminal::get_color_at_index(*i as usize, theme)
1039 }
1040 }
1041}