Skip to main content

gpui_cn/terminal/
element.rs

1//! The terminal element: one canvas that paints the grid.
2//!
3//! Everything the paint phase needs is built during prepaint so painting
4//! never reads the entity or the engine.
5//!
6//! Derived from Herdr (Apache-2.0). The canvas structure, cell-aligned glyph
7//! positioning and merged background quads are adapted from Muxy v2 (MIT).
8
9use 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
39/// Zero-width non-joiner separating runs so shaping cannot form a ligature
40/// across a style boundary.
41const SEPARATOR: char = '\u{200c}';
42
43/// The key context actions are bound in.
44pub const KEY_CONTEXT: &str = "Terminal";
45
46/// The terminal grid, drawn for a [`TerminalState`]. It fills its parent
47/// unless styled otherwise.
48#[derive(IntoElement)]
49pub struct Terminal {
50    id: ElementId,
51    state: Entity<TerminalState>,
52    style: StyleRefinement,
53}
54
55impl Terminal {
56    /// An element `id` that draws `state`.
57    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/// Everything the paint phase needs.
214#[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/// Each row's prepared glyphs and quads from the last frame, relative to
229/// the row's top left, so a row that did not change is not built again:
230/// not on a cursor blink, a hover, or a scroll that moves it.
231///
232/// A row is keyed by its stable id, its revision and its selected columns.
233/// Everything a row's look depends on beyond those (the font, its size, the
234/// cell, the scale and the colors) is the cache's `look`; a change to it
235/// clears the cache. The cache keeps only the rows the last frame drew, so
236/// it never holds more than one screen.
237#[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/// One row's share of a [`Painting`], at row zero of a grid at the origin.
261#[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    /// Moves this row's parts by `offset` into the frame's painting.
271    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            // A scroll area around the terminal takes the wheel in the
378            // capture phase, from the outside in, so the terminal takes
379            // its steps there too, and its stop keeps them from the page.
380            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/// The theme values the paint phase needs.
427#[derive(Clone, Copy)]
428struct Look {
429    cursor_thickness: Pixels,
430    cursor_opacity: f32,
431}
432
433/// A hairline under a span of cells, `offset` above the cell bottom.
434fn 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    // Rows are prepared at row zero of a grid at the origin and moved into
483    // place, so a cached row fits wherever it is now.
484    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        // Composition text is drawn at the cursor with an underline.
530        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
565/// The cursor to draw, in Ghostty's order (`src/renderer/cursor.zig`): a
566/// terminal without focus always shows a hollow block, so a split shows
567/// where each pane's cursor sits; one with focus shows `shape` while the
568/// blink is on.
569fn 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
701/// Append one style run's text, recording the grid column each byte belongs
702/// to so glyphs can be snapped back onto cell boundaries after shaping.
703fn 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
763/// Snap each glyph back to its grid column. Proportional fallback fonts,
764/// CJK glyphs and combining marks otherwise drift away from the grid.
765fn 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
792/// Merge a quad into the previous one when they are horizontally adjacent
793/// and share a color.
794fn 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}