1use std::collections::HashMap;
10use std::sync::Arc;
11
12use gpui_kit::base::{StyledExt as _, TestSupportExt as _};
13use gpui_kit::prelude::FluentBuilder as _;
14use gpui_kit::{
15 App, BorderStyle, Bounds, ContentMask, DispatchPhase, ElementId, ElementInputHandler, Entity,
16 EntityInputHandler as _, Font, FontStyle, FontWeight, HitboxBehavior, Hsla, InteractiveElement,
17 IntoElement, LineLayout, ModifiersChangedEvent, MouseButton, MouseMoveEvent, MouseUpEvent,
18 ParentElement, Pixels, Point, RenderOnce, ScrollEvent, ShapedLine, Size,
19 StatefulInteractiveElement as _, StyleRefinement, Styled, TextAlign, TextRun, Window, canvas,
20 div, fill, outline, point, px, size,
21};
22
23use crate::theme::ActiveTheme as _;
24
25use crate::terminal::actions::{
26 Clear, Copy, DecreaseFontSize, IncreaseFontSize, Paste, ResetFontSize, ScrollLineDown,
27 ScrollLineUp, ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, SelectAll,
28};
29use crate::terminal::block;
30use crate::terminal::colors::Palette;
31use crate::terminal::decoration;
32use crate::terminal::frame::{CursorShape, StyleRun, TerminalColors, TerminalFrame, TerminalRow};
33use crate::terminal::geometry::Geometry;
34use crate::terminal::input::ScrollRequest;
35use crate::terminal::selection::Selection;
36use crate::terminal::state::TerminalState;
37use ghostty_vt::render::RowId;
38
39const SEPARATOR: char = '\u{200c}';
42
43pub const KEY_CONTEXT: &str = "Terminal";
45
46#[derive(IntoElement)]
49pub struct Terminal {
50 id: ElementId,
51 state: Entity<TerminalState>,
52 style: StyleRefinement,
53}
54
55impl Terminal {
56 pub fn new(id: impl Into<ElementId>, state: &Entity<TerminalState>) -> Self {
58 Self {
59 id: id.into(),
60 state: state.clone(),
61 style: StyleRefinement::default(),
62 }
63 }
64}
65
66impl Styled for Terminal {
67 fn style(&mut self) -> &mut StyleRefinement {
68 &mut self.style
69 }
70}
71
72fn update<E: 'static>(
73 state: &Entity<TerminalState>,
74 handler: impl Fn(&mut TerminalState, &E, &mut Window, &mut gpui_kit::Context<TerminalState>)
75 + 'static,
76) -> impl Fn(&E, &mut Window, &mut App) + 'static {
77 let state = state.clone();
78 move |event, window, cx| state.update(cx, |s, cx| handler(s, event, window, cx))
79}
80
81impl RenderOnce for Terminal {
82 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
83 let state = self.state;
84 let (focus, hovered, message, background) = {
85 let s = state.read(cx);
86 (
87 s.focus_handle().clone(),
88 s.hovered_link().is_some(),
89 s.status_message(),
90 Palette::new(&s.frame().colors).background(),
91 )
92 };
93 let focused = focus.is_focused(window);
94 let status_id = ElementId::NamedChild(Arc::new(self.id.clone()), "status".into());
95 let theme = cx.theme();
96 let (pill, pill_text, radius, text_size) = (
97 theme.tooltip,
98 theme.tooltip_foreground,
99 theme.radius_sm(),
100 theme.text_control.size,
101 );
102 div()
103 .id(self.id)
104 .test_support()
105 .key_context(KEY_CONTEXT)
106 .track_focus(&focus)
107 .relative()
108 .size_full()
109 .overflow_hidden()
110 .role(gpui_kit::Role::Terminal)
111 .aria_label("Terminal")
112 .aria_value("")
113 .on_a11y_action(gpui_kit::AccessibleAction::SetValue, {
114 let state = state.clone();
115 move |data, window, cx| {
116 let Some(gpui_kit::accesskit::ActionData::Value(text)) = data else {
117 return;
118 };
119 state.update(cx, |s, cx| s.replace_text_in_range(None, text, window, cx));
120 }
121 })
122 .bg(background)
123 .refine_style(&self.style)
124 .on_action(update(&state, |s, _: &Copy, _, cx| {
125 if s.has_selection() {
126 s.copy(cx);
127 } else {
128 cx.propagate();
129 }
130 }))
131 .on_action(update(&state, |s, _: &Paste, _, cx| s.paste(cx)))
132 .on_action(update(&state, |s, _: &SelectAll, _, cx| s.select_all(cx)))
133 .on_action(update(&state, |s, _: &Clear, _, cx| s.clear(cx)))
134 .on_action(update(&state, |s, _: &IncreaseFontSize, _, cx| {
135 s.increase_font_size(px(1.), cx);
136 }))
137 .on_action(update(&state, |s, _: &DecreaseFontSize, _, cx| {
138 s.decrease_font_size(px(1.), cx);
139 }))
140 .on_action(update(&state, |s, _: &ResetFontSize, _, cx| {
141 s.reset_font_size(cx);
142 }))
143 .on_action(update(&state, |s, _: &ScrollToTop, _, cx| {
144 s.scroll(ScrollRequest::Top, cx);
145 }))
146 .on_action(update(&state, |s, _: &ScrollToBottom, _, cx| {
147 s.scroll(ScrollRequest::Bottom, cx);
148 }))
149 .on_action(update(&state, |s, _: &ScrollPageUp, _, cx| {
150 s.scroll_pages(-1, cx);
151 }))
152 .on_action(update(&state, |s, _: &ScrollPageDown, _, cx| {
153 s.scroll_pages(1, cx);
154 }))
155 .on_action(update(&state, |s, _: &ScrollLineUp, _, cx| {
156 s.scroll(ScrollRequest::Lines(-1), cx);
157 }))
158 .on_action(update(&state, |s, _: &ScrollLineDown, _, cx| {
159 s.scroll(ScrollRequest::Lines(1), cx);
160 }))
161 .when(hovered, Styled::cursor_pointer)
162 .on_modifiers_changed(update(&state, |s, event: &ModifiersChangedEvent, w, cx| {
163 s.hover_link(w.mouse_position(), event.modifiers.platform, cx);
164 }))
165 .on_key_down(update(&state, TerminalState::on_key_down))
166 .on_key_up(update(&state, TerminalState::on_key_up))
167 .on_mouse_down(
168 MouseButton::Left,
169 update(&state, TerminalState::on_mouse_down),
170 )
171 .on_mouse_down(
172 MouseButton::Middle,
173 update(&state, TerminalState::on_mouse_down),
174 )
175 .on_mouse_down(
176 MouseButton::Right,
177 update(&state, TerminalState::on_mouse_down),
178 )
179 .on_mouse_move(update(&state, TerminalState::on_mouse_move))
180 .on_mouse_up(
181 MouseButton::Left,
182 update(&state, TerminalState::on_mouse_up),
183 )
184 .on_mouse_up(
185 MouseButton::Middle,
186 update(&state, TerminalState::on_mouse_up),
187 )
188 .on_mouse_up(
189 MouseButton::Right,
190 update(&state, TerminalState::on_mouse_up),
191 )
192 .child(grid(state, focused))
193 .when_some(message, |element, message| {
194 element.child(
195 div()
196 .id(status_id)
197 .test_support()
198 .absolute()
199 .bottom_2()
200 .left_2()
201 .px_2()
202 .py_1()
203 .rounded(radius)
204 .bg(pill)
205 .text_color(pill_text)
206 .text_size(text_size)
207 .child(message),
208 )
209 })
210 }
211}
212
213#[derive(Default)]
215struct Painting {
216 backgrounds: Vec<(Bounds<Pixels>, Hsla)>,
217 selections: Vec<Bounds<Pixels>>,
218 blocks: Vec<(Bounds<Pixels>, Hsla)>,
219 decorations: Vec<(Bounds<Pixels>, Hsla)>,
220 lines: Vec<(Point<Pixels>, ShapedLine)>,
221 cursor: Option<(Bounds<Pixels>, Hsla, CursorShape)>,
222 cell_height: Pixels,
223 selection: Hsla,
224 cursor_thickness: Pixels,
225 cursor_opacity: f32,
226}
227
228#[derive(Default)]
238pub(crate) struct RowCache {
239 look: Option<RowLook>,
240 rows: HashMap<RowKey, Arc<RowPaint>>,
241}
242
243#[derive(Clone, PartialEq)]
244struct RowLook {
245 font: Font,
246 font_size: Pixels,
247 cell: Size<Pixels>,
248 columns: u16,
249 scale: f32,
250 colors: TerminalColors,
251}
252
253#[derive(Clone, PartialEq, Eq, Hash)]
254struct RowKey {
255 id: RowId,
256 revision: u64,
257 selected: std::ops::Range<u16>,
258}
259
260#[derive(Default)]
262struct RowPaint {
263 backgrounds: Vec<(Bounds<Pixels>, Hsla)>,
264 blocks: Vec<(Bounds<Pixels>, Hsla)>,
265 decorations: Vec<(Bounds<Pixels>, Hsla)>,
266 line: Option<ShapedLine>,
267}
268
269impl RowPaint {
270 fn emit(&self, offset: Point<Pixels>, painting: &mut Painting) {
272 let moved = |(bounds, color): &(Bounds<Pixels>, Hsla)| {
273 (Bounds::new(bounds.origin + offset, bounds.size), *color)
274 };
275 painting
276 .backgrounds
277 .extend(self.backgrounds.iter().map(moved));
278 painting.blocks.extend(self.blocks.iter().map(moved));
279 painting
280 .decorations
281 .extend(self.decorations.iter().map(moved));
282 if let Some(line) = &self.line {
283 painting.lines.push((offset, line.clone()));
284 }
285 }
286}
287
288fn grid(state: Entity<TerminalState>, focused: bool) -> impl IntoElement {
289 let paint_state = state.clone();
290 canvas(
291 move |bounds, window, cx| {
292 let theme = cx.theme();
293 let look = Look {
294 cursor_thickness: theme.metrics.terminal_cursor_thickness,
295 cursor_opacity: theme.terminal_cursor_opacity,
296 };
297 let (frame, appearance, marked) = {
298 let s = state.read(cx);
299 (
300 Arc::clone(s.frame()),
301 s.drawn_appearance(cx.theme()),
302 s.marked_text().map(str::to_owned),
303 )
304 };
305 let font = appearance.font();
306 let font_size = appearance.size();
307 let cell = appearance.cell_size(window);
308 let geometry =
309 Geometry::new(bounds, cell, appearance.inset(), appearance.padding_balance);
310 let scale = window.scale_factor();
311 {
312 let s = state.read(cx);
313 if s.geometry != Some(geometry) || s.geometry_scale() != Some(scale) {
314 let deferred = state.downgrade();
315 window.defer(cx, move |_, cx| {
316 let _ = deferred.update(cx, |s, cx| s.set_geometry(geometry, scale, cx));
317 });
318 }
319 }
320 let active = window.is_window_active();
321 let reduce_motion = cx.reduce_motion();
322 let on_screen = !window
323 .content_mask()
324 .bounds
325 .intersect(&geometry.bounds)
326 .is_empty();
327 state.update(cx, |s, cx| {
328 let blinking = focused
329 && active
330 && on_screen
331 && !reduce_motion
332 && s.is_visible()
333 && s.is_live()
334 && marked.is_none()
335 && cursor_in_grid(s.frame(), geometry)
336 && s.appearance().cursor_blinks(s.frame().cursor.blinking);
337 s.cursor.sync(blinking, cx, |s, cx| {
338 s.cursor.tick();
339 cx.notify();
340 });
341 });
342 let mut rows = state.update(cx, |s, _| std::mem::take(&mut s.row_cache));
343 let s = state.read(cx);
344 let mut painting = prepare(
345 &frame,
346 geometry,
347 look,
348 s.selection,
349 cursor_style(
350 focused,
351 s.cursor.visible,
352 appearance.cursor_shape.unwrap_or(frame.cursor.shape),
353 ),
354 marked.as_deref(),
355 &font,
356 font_size,
357 &mut rows,
358 window,
359 );
360 if let Some((row, columns)) = s.hovered_link_span()
361 && row < geometry.rows
362 {
363 let columns = columns.start..columns.end.min(geometry.columns);
364 if columns.start < columns.end {
365 let bounds = geometry.span_bounds(columns, row);
366 painting.decorations.push((
367 underline(bounds, look.cursor_thickness),
368 Palette::new(&frame.colors).foreground(),
369 ));
370 }
371 }
372 state.update(cx, |s, _| s.row_cache = rows);
373 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
374 (painting, geometry, hitbox)
375 },
376 move |_bounds, (painting, geometry, hitbox), window, cx| {
377 let scroll_state = paint_state.downgrade();
381 window.on_mouse_event(move |event: &ScrollEvent, phase, window, cx| {
382 if phase == DispatchPhase::Capture && hitbox.should_handle_scroll(window) {
383 let _ = scroll_state.update(cx, |s, cx| s.on_scroll(event, window, cx));
384 }
385 });
386 let drag_state = paint_state.downgrade();
387 window.on_mouse_event(move |event: &MouseMoveEvent, phase, _, cx| {
388 if phase == DispatchPhase::Capture
389 && event.pressed_button == Some(MouseButton::Left)
390 {
391 let _ = drag_state.update(cx, |s, cx| s.drag_selection(event.position, cx));
392 }
393 });
394 let release_state = paint_state.downgrade();
395 window.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| {
396 if phase == DispatchPhase::Capture && event.button == MouseButton::Left {
397 let _ =
398 release_state.update(cx, |s, cx| s.finish_selection(event.position, cx));
399 }
400 });
401 let focus = paint_state.read(cx).focus_handle().clone();
402 window.handle_input(
403 &focus,
404 ElementInputHandler::new(geometry.grid_bounds(), paint_state.clone()),
405 cx,
406 );
407 let mut painted = false;
408 window.with_content_mask(
409 Some(ContentMask {
410 bounds: geometry.bounds,
411 }),
412 |window| {
413 painted = !window.content_mask().bounds.is_empty();
414 paint(painting, window, cx);
415 },
416 );
417 let painted_state = paint_state.downgrade();
418 window.defer(cx, move |_, cx| {
419 let _ = painted_state.update(cx, |s, _| s.frame_painted(painted));
420 });
421 },
422 )
423 .size_full()
424}
425
426#[derive(Clone, Copy)]
428struct Look {
429 cursor_thickness: Pixels,
430 cursor_opacity: f32,
431}
432
433fn underline(span: Bounds<Pixels>, offset: Pixels) -> Bounds<Pixels> {
435 Bounds::new(
436 span.origin + point(px(0.0), span.size.height - offset),
437 size(span.size.width, px(1.0)),
438 )
439}
440
441impl TerminalState {
442 fn geometry_scale(&self) -> Option<f32> {
443 self.geometry.map(|_| self.geometry_scale_value())
444 }
445}
446
447#[allow(clippy::too_many_arguments)]
448fn prepare(
449 frame: &TerminalFrame,
450 geometry: Geometry,
451 look: Look,
452 selection: Option<Selection>,
453 cursor: Option<CursorShape>,
454 marked: Option<&str>,
455 font: &Font,
456 font_size: Pixels,
457 cache: &mut RowCache,
458 window: &mut Window,
459) -> Painting {
460 let palette = Palette::new(&frame.colors);
461 let mut painting = Painting {
462 cell_height: geometry.cell.height,
463 selection: palette.selection(),
464 cursor_thickness: look.cursor_thickness,
465 cursor_opacity: look.cursor_opacity,
466 ..Painting::default()
467 };
468 let scale = window.scale_factor();
469
470 let row_look = RowLook {
471 font: font.clone(),
472 font_size,
473 cell: geometry.cell,
474 columns: geometry.columns,
475 scale,
476 colors: frame.colors.clone(),
477 };
478 if cache.look.as_ref() != Some(&row_look) {
479 cache.look = Some(row_look);
480 cache.rows.clear();
481 }
482 let at_origin = Geometry {
485 origin: point(px(0.0), px(0.0)),
486 ..geometry
487 };
488 let mut drawn = HashMap::with_capacity(usize::from(geometry.rows));
489 let visible = usize::from(geometry.rows).min(frame.rows.len());
490 for (index, row) in frame.rows.iter().take(visible).enumerate() {
491 #[allow(clippy::cast_possible_truncation)]
492 let row_index = index as u16;
493 let selected = selection
494 .map(|selection| selection.columns(row_index, geometry.columns))
495 .unwrap_or(0..0);
496 if !selected.is_empty() {
497 painting
498 .selections
499 .push(geometry.span_bounds(selected.clone(), row_index));
500 }
501 let key = RowKey {
502 id: row.id,
503 revision: row.revision,
504 selected: selected.clone(),
505 };
506 let row_paint = cache.rows.remove(&key).unwrap_or_else(|| {
507 let mut row_paint = RowPaint::default();
508 prepare_row(
509 row,
510 at_origin,
511 palette,
512 selected,
513 font,
514 font_size,
515 scale,
516 window,
517 &mut row_paint,
518 );
519 Arc::new(row_paint)
520 });
521 row_paint.emit(geometry.row_origin(row_index), &mut painting);
522 drawn.insert(key, row_paint);
523 }
524 cache.rows = drawn;
525
526 if let Some(marked) = marked.filter(|m| !m.is_empty())
527 && cursor_in_grid(frame, geometry)
528 {
529 let start = frame.cursor.column;
531 let columns = u16::try_from(marked.chars().count()).unwrap_or(u16::MAX);
532 let end = start.saturating_add(columns).min(geometry.columns);
533 let bounds = geometry.span_bounds(start..end, frame.cursor.row);
534 painting.backgrounds.push((bounds, palette.background()));
535 let line = window.text_system().shape_line(
536 marked.to_owned().into(),
537 font_size,
538 &[TextRun {
539 len: marked.len(),
540 font: font.clone(),
541 color: palette.foreground(),
542 background_color: None,
543 underline: None,
544 strikethrough: None,
545 }],
546 None,
547 );
548 painting.lines.push((bounds.origin, line));
549 painting.decorations.push((
550 underline(bounds, look.cursor_thickness),
551 palette.foreground(),
552 ));
553 return painting;
554 }
555
556 if let Some(shape) = cursor
557 && cursor_in_grid(frame, geometry)
558 {
559 let bounds = geometry.cell_bounds(frame.cursor.column, frame.cursor.row);
560 painting.cursor = Some((bounds, palette.cursor(), shape));
561 }
562 painting
563}
564
565fn cursor_style(focused: bool, blink_on: bool, shape: CursorShape) -> Option<CursorShape> {
570 if !focused {
571 Some(CursorShape::Hollow)
572 } else if blink_on {
573 Some(shape)
574 } else {
575 None
576 }
577}
578
579fn cursor_in_grid(frame: &TerminalFrame, geometry: Geometry) -> bool {
580 frame.cursor.visible
581 && frame.scroll.at_bottom()
582 && frame.cursor.column < geometry.columns
583 && frame.cursor.row < geometry.rows
584}
585
586#[allow(clippy::too_many_arguments)]
587fn prepare_row(
588 row: &TerminalRow,
589 geometry: Geometry,
590 palette: Palette<'_>,
591 selected: std::ops::Range<u16>,
592 font: &Font,
593 font_size: Pixels,
594 scale: f32,
595 window: &mut Window,
596 painting: &mut RowPaint,
597) {
598 let row_index = 0;
599 let mut text = String::new();
600 let mut runs: Vec<TextRun> = Vec::new();
601 let mut columns: Vec<u16> = Vec::new();
602 let mut last_column = 0_u16;
603
604 for run in row.runs.iter() {
605 if run.columns.start >= geometry.columns {
606 break;
607 }
608 let end = run.columns.end.min(geometry.columns);
609 if end <= run.columns.start {
610 continue;
611 }
612 let (foreground, background) = palette.style(&run.style);
613 let bounds = geometry.span_bounds(run.columns.start..end, row_index);
614 if background != palette.background() {
615 merge_quad(&mut painting.backgrounds, bounds, background);
616 }
617 last_column = end;
618 if run.style.invisible {
619 continue;
620 }
621 let first = row
622 .cells
623 .partition_point(|cell| cell.column < run.columns.start);
624 let mut pending: Option<(StyleRun, Hsla)> = None;
625 for cell in row.cells[first..]
626 .iter()
627 .take_while(|cell| cell.column < end)
628 .filter(|cell| !cell.spacer)
629 {
630 let cell_end = cell.column.saturating_add(u16::from(cell.width)).min(end);
631 let cell_bounds = geometry.span_bounds(cell.column..cell_end, row_index);
632 let color = if selected.start < cell_end && cell.column < selected.end {
633 palette.selection_foreground()
634 } else {
635 foreground
636 };
637 let cell_text = row.text.get(cell.text.clone()).unwrap_or_default();
638 let quads = block::quads(cell_text, cell_bounds, color, scale);
639 if (quads.is_some() || pending.as_ref().is_some_and(|(_, p)| *p != color))
640 && let Some((span, color)) = pending.take()
641 {
642 append_run(row, &span, color, font, &mut text, &mut runs, &mut columns);
643 }
644 if let Some(quads) = quads {
645 for (bounds, color) in quads {
646 merge_quad(&mut painting.blocks, bounds, color);
647 }
648 } else if let Some((span, _)) = pending.as_mut() {
649 span.text.end = cell.text.end;
650 span.columns.end = cell_end;
651 } else {
652 let mut span = run.clone();
653 span.text = cell.text.clone();
654 span.columns = cell.column..cell_end;
655 pending = Some((span, color));
656 }
657 }
658 if let Some((span, color)) = pending {
659 append_run(row, &span, color, font, &mut text, &mut runs, &mut columns);
660 }
661 let selected_start = selected.start.clamp(run.columns.start, end);
662 let selected_end = selected.end.clamp(selected_start, end);
663 for (span, in_selection) in [
664 (run.columns.start..selected_start, false),
665 (selected_start..selected_end, true),
666 (selected_end..end, false),
667 ] {
668 if span.is_empty() {
669 continue;
670 }
671 let color = if in_selection {
672 palette.selection_foreground()
673 } else {
674 foreground
675 };
676 let underline = if in_selection {
677 palette.selection_foreground()
678 } else {
679 palette.underline(&run.style)
680 };
681 decoration::prepare(
682 &mut painting.decorations,
683 &run.style,
684 geometry.span_bounds(span, row_index),
685 color,
686 underline,
687 scale,
688 );
689 }
690 }
691
692 if !text.is_empty() {
693 let mut line = window
694 .text_system()
695 .shape_line(text.into(), font_size, &runs, None);
696 align_to_cells(&mut line, &columns, geometry.cell.width, last_column);
697 painting.line = Some(line);
698 }
699}
700
701fn append_run(
704 row: &TerminalRow,
705 run: &StyleRun,
706 color: Hsla,
707 font: &Font,
708 text: &mut String,
709 runs: &mut Vec<TextRun>,
710 columns: &mut Vec<u16>,
711) {
712 let run_text = row.text.get(run.text.clone()).unwrap_or_default();
713 if run_text.is_empty() || run_text.bytes().all(|byte| byte == b' ') {
714 return;
715 }
716 let start = text.len();
717 text.push_str(run_text);
718 text.push(SEPARATOR);
719
720 let mut byte = run.text.start;
721 let first = row
722 .cells
723 .partition_point(|cell| cell.column < run.columns.start);
724 for cell in row.cells[first..]
725 .iter()
726 .take_while(|cell| cell.column < run.columns.end)
727 {
728 if cell.spacer || cell.text.end <= run.text.start || cell.text.start >= run.text.end {
729 continue;
730 }
731 let cell_start = cell.text.start.max(run.text.start);
732 let cell_end = cell.text.end.min(run.text.end);
733 columns.extend(std::iter::repeat_n(cell.column, cell_end - cell_start));
734 byte = cell_end;
735 }
736 columns.extend(std::iter::repeat_n(
737 run.columns.start,
738 run.text.end.saturating_sub(byte),
739 ));
740 columns.resize(text.len() - SEPARATOR.len_utf8(), run.columns.start);
741 columns.extend(std::iter::repeat_n(
742 run.columns.end.saturating_sub(1),
743 SEPARATOR.len_utf8(),
744 ));
745
746 let mut font = font.clone();
747 if run.style.bold {
748 font.weight = FontWeight::BOLD;
749 }
750 if run.style.italic {
751 font.style = FontStyle::Italic;
752 }
753 runs.push(TextRun {
754 len: text.len() - start,
755 font,
756 color,
757 background_color: None,
758 underline: None,
759 strikethrough: None,
760 });
761}
762
763fn align_to_cells(line: &mut ShapedLine, columns: &[u16], cell_width: Pixels, width: u16) {
766 let mut runs = line.runs.clone();
767 let mut anchors: HashMap<u16, Pixels> = HashMap::new();
768 for glyph in runs.iter().flat_map(|run| &run.glyphs) {
769 if let Some(column) = columns.get(glyph.index) {
770 anchors.entry(*column).or_insert(glyph.position.x);
771 }
772 }
773 for run in &mut runs {
774 for glyph in &mut run.glyphs {
775 if let Some(column) = columns.get(glyph.index)
776 && let Some(anchor) = anchors.get(column)
777 {
778 glyph.position.x = cell_width * f32::from(*column) + glyph.position.x - *anchor;
779 }
780 }
781 }
782 **line = Arc::new(LineLayout {
783 font_size: line.font_size,
784 width: cell_width * f32::from(width),
785 ascent: line.ascent,
786 descent: line.descent,
787 runs,
788 len: line.len(),
789 });
790}
791
792fn merge_quad(quads: &mut Vec<(Bounds<Pixels>, Hsla)>, bounds: Bounds<Pixels>, color: Hsla) {
795 if let Some((previous, previous_color)) = quads.last_mut()
796 && *previous_color == color
797 && previous.origin.y == bounds.origin.y
798 && previous.right() == bounds.left()
799 && previous.size.height == bounds.size.height
800 {
801 previous.size.width += bounds.size.width;
802 return;
803 }
804 quads.push((bounds, color));
805}
806
807fn paint(painting: Painting, window: &mut Window, cx: &mut App) {
808 for (bounds, color) in painting.backgrounds {
809 window.paint_quad(fill(bounds, color));
810 }
811 for bounds in painting.selections {
812 window.paint_quad(fill(bounds, painting.selection));
813 }
814 for (bounds, color) in painting.decorations {
815 window.paint_quad(fill(bounds, color));
816 }
817 for (bounds, color) in painting.blocks {
818 window.paint_quad(fill(bounds, color));
819 }
820 for (origin, line) in painting.lines {
821 let _ = line.paint(
822 origin,
823 painting.cell_height,
824 TextAlign::Left,
825 None,
826 window,
827 cx,
828 );
829 }
830 if let Some((bounds, color, shape)) = painting.cursor {
831 let thickness = painting.cursor_thickness;
832 match shape {
833 CursorShape::Block => {
834 window.paint_quad(fill(bounds, color.opacity(painting.cursor_opacity)));
835 }
836 CursorShape::Hollow => {
837 window.paint_quad(outline(bounds, color, BorderStyle::Solid));
838 }
839 CursorShape::Bar => window.paint_quad(fill(
840 Bounds::new(bounds.origin, size(thickness, bounds.size.height)),
841 color,
842 )),
843 CursorShape::Underline => window.paint_quad(fill(
844 Bounds::new(
845 bounds.origin + point(px(0.0), bounds.size.height - thickness),
846 size(bounds.size.width, thickness),
847 ),
848 color,
849 )),
850 }
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use super::{cursor_style, merge_quad};
857 use crate::terminal::frame::CursorShape;
858
859 #[test]
860 fn a_terminal_without_focus_shows_a_hollow_block_through_the_blink() {
861 assert_eq!(
862 cursor_style(false, true, CursorShape::Bar),
863 Some(CursorShape::Hollow)
864 );
865 assert_eq!(
866 cursor_style(false, false, CursorShape::Bar),
867 Some(CursorShape::Hollow)
868 );
869 assert_eq!(
870 cursor_style(true, true, CursorShape::Bar),
871 Some(CursorShape::Bar)
872 );
873 assert_eq!(cursor_style(true, false, CursorShape::Bar), None);
874 }
875 use gpui_kit::{Bounds, Hsla, point, px, size};
876
877 #[test]
878 fn adjacent_quads_of_one_color_merge_and_others_stay_separate() {
879 let mut quads = Vec::new();
880 let white = Hsla::white();
881 merge_quad(
882 &mut quads,
883 Bounds::new(point(px(0.0), px(0.0)), size(px(8.0), px(16.0))),
884 white,
885 );
886 merge_quad(
887 &mut quads,
888 Bounds::new(point(px(8.0), px(0.0)), size(px(8.0), px(16.0))),
889 white,
890 );
891 assert_eq!(quads.len(), 1);
892 assert_eq!(quads[0].0.size.width, px(16.0));
893 merge_quad(
894 &mut quads,
895 Bounds::new(point(px(16.0), px(0.0)), size(px(8.0), px(16.0))),
896 Hsla::black(),
897 );
898 merge_quad(
899 &mut quads,
900 Bounds::new(point(px(0.0), px(16.0)), size(px(8.0), px(16.0))),
901 white,
902 );
903 assert_eq!(quads.len(), 3);
904 }
905}