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