Skip to main content

gpui/text_system/
line.rs

1use crate::{
2    App, Bounds, InlineLayout, LineLayout, PaintFragment, PaintQuad, Pixels, Point, Result,
3    ShapedTextLayout, SharedString, TextAlign, TextSystem, VisualLine, Window, point, size,
4};
5use derive_more::{Deref, DerefMut};
6use std::sync::{
7    Arc,
8    atomic::{AtomicUsize, Ordering},
9};
10
11static GLYPH_PAINT_ERRORS: AtomicUsize = AtomicUsize::new(0);
12const MAX_LOGGED_GLYPH_PAINT_ERRORS: usize = 20;
13
14/// A shaped line that is ready to measure and paint.
15///
16/// [`WindowTextSystem::shape_line`](crate::WindowTextSystem::shape_line) does
17/// the expensive shaping work. Reuse the result to measure with
18/// [`ShapedLine::width`] and paint on each frame. Paint backgrounds first with
19/// [`ShapedLine::paint_background`] when a row needs to sit behind other content.
20///
21/// # Examples
22///
23/// ```
24/// # use gpui::{EmptyView, TestApp, TextRun, font, hsla, px};
25/// # let mut app = TestApp::new();
26/// # let mut window = app.open_window(|_window, _cx| EmptyView);
27/// window.update(|_view, window, _cx| {
28///     let run = TextRun {
29///         len: "Hi".len(),
30///         font: font("Lilex"),
31///         color: hsla(0., 0., 0., 1.),
32///         ..Default::default()
33///     };
34///     let line = window.text_system().shape_line("Hi".into(), px(16.), &[run]);
35///     assert_eq!(line.text.as_ref(), "Hi");
36///     assert_eq!(line.len(), 2);
37/// });
38/// ```
39#[derive(Clone, Debug, Deref, DerefMut)]
40pub struct ShapedLine {
41    #[deref]
42    #[deref_mut]
43    pub(crate) layout: Arc<LineLayout>,
44    /// The text that was shaped for this line.
45    pub text: SharedString,
46}
47
48impl ShapedLine {
49    /// Returns the length of the line in UTF-8 bytes.
50    #[allow(clippy::len_without_is_empty)]
51    pub fn len(&self) -> usize {
52        self.layout.len
53    }
54
55    /// Returns the width of the line in logical pixels.
56    ///
57    /// This is the glyph advance width from the text shaping system. Use it to
58    /// advance a "pen" when you paint several lines or fragments on one row.
59    pub fn width(&self) -> Pixels {
60        self.layout.width
61    }
62
63    /// Paints the glyphs, underlines, and strikethroughs of the line.
64    ///
65    /// `origin` is the top-left corner of the line box, which is `line_height`
66    /// tall. The baseline is placed inside that box, centered by the font's
67    /// ascent and descent. When `align_width` is `Some`, the line is aligned
68    /// within that width using `align`. Otherwise it is aligned in its own width.
69    ///
70    /// Call this during paint. Background colors are not painted here, so call
71    /// [`ShapedLine::paint_background`] first.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error only if the window's paint layer fails. A glyph that
76    /// fails to paint is logged and skipped, and the call still succeeds.
77    pub fn paint(
78        &self,
79        origin: Point<Pixels>,
80        line_height: Pixels,
81        align: TextAlign,
82        align_width: Option<Pixels>,
83        window: &mut Window,
84        cx: &mut App,
85    ) -> Result<()> {
86        paint_visual_text(
87            origin,
88            &self.layout,
89            line_height,
90            align,
91            align_width,
92            &self.layout.visual_lines,
93            TextPaintPass::Foreground,
94            window,
95            cx,
96        )
97    }
98
99    /// Paints the background color of each run in the line.
100    ///
101    /// The arguments match [`ShapedLine::paint`]. Call this before `paint`, so
102    /// that the glyphs are drawn over the backgrounds.
103    pub fn paint_background(
104        &self,
105        origin: Point<Pixels>,
106        line_height: Pixels,
107        align: TextAlign,
108        align_width: Option<Pixels>,
109        window: &mut Window,
110        cx: &mut App,
111    ) -> Result<()> {
112        paint_visual_text(
113            origin,
114            &self.layout,
115            line_height,
116            align,
117            align_width,
118            &self.layout.visual_lines,
119            TextPaintPass::Background,
120            window,
121            cx,
122        )
123    }
124}
125
126/// Shaped text that may contain hard breaks and soft-wrapped lines.
127///
128/// [`WindowTextSystem::shape_text`](crate::WindowTextSystem::shape_text) returns
129/// one visual line for each wrapped or hard-broken line. Paint it with
130/// [`ShapedText::paint`], and paint backgrounds first with
131/// [`ShapedText::paint_background`]. It derefs to [`ShapedTextLayout`], so the
132/// same result supplies measurements such as [`ShapedTextLayout::size`] and
133/// [`ShapedTextLayout::line_count`].
134///
135/// # Examples
136///
137/// ```
138/// # use gpui::{EmptyView, TestApp, TextRun, font, hsla, px};
139/// # let mut app = TestApp::new();
140/// # let mut window = app.open_window(|_window, _cx| EmptyView);
141/// window.update(|_view, window, _cx| {
142///     let text = "Hello\nworld";
143///     let run = TextRun {
144///         len: text.len(),
145///         font: font("Lilex"),
146///         color: hsla(0., 0., 0., 1.),
147///         ..Default::default()
148///     };
149///     let shaped = window
150///         .text_system()
151///         .shape_text(text, px(16.), &[run], None, None)
152///         .unwrap();
153///     assert_eq!(shaped.len(), text.len());
154/// });
155/// ```
156#[derive(Debug, Deref, DerefMut)]
157pub struct ShapedText {
158    #[deref]
159    #[deref_mut]
160    pub(crate) layout: Arc<ShapedTextLayout>,
161    /// The text that was shaped.
162    pub text: SharedString,
163}
164
165impl ShapedText {
166    /// Returns the length of the text in UTF-8 bytes.
167    #[allow(clippy::len_without_is_empty)]
168    pub fn len(&self) -> usize {
169        self.layout.len()
170    }
171
172    /// Paints the glyphs and decorations of every visual line.
173    ///
174    /// `origin` is the top-left corner of the first line. Each following line
175    /// starts `line_height` below the previous one. `bounds` supplies the width
176    /// used for alignment; when it is `None`, the layout's wrap width is used.
177    /// See [`ShapedLine::paint`] for how the arguments place the text.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error only if the window's paint layer fails. A glyph that
182    /// fails to paint is logged and skipped.
183    pub fn paint(
184        &self,
185        origin: Point<Pixels>,
186        line_height: Pixels,
187        align: TextAlign,
188        bounds: Option<Bounds<Pixels>>,
189        window: &mut Window,
190        cx: &mut App,
191    ) -> Result<()> {
192        let align_width = match bounds {
193            Some(bounds) => Some(bounds.size.width),
194            None => self.layout.wrap_width,
195        };
196
197        paint_visual_text(
198            origin,
199            &self.layout.layout,
200            line_height,
201            align,
202            align_width,
203            &self.layout.visual_lines,
204            TextPaintPass::Foreground,
205            window,
206            cx,
207        )
208    }
209
210    /// Paints the background colors of every visual line before the foreground.
211    /// The arguments have the same meaning as [`ShapedText::paint`].
212    pub fn paint_background(
213        &self,
214        origin: Point<Pixels>,
215        line_height: Pixels,
216        align: TextAlign,
217        bounds: Option<Bounds<Pixels>>,
218        window: &mut Window,
219        cx: &mut App,
220    ) -> Result<()> {
221        let align_width = match bounds {
222            Some(bounds) => Some(bounds.size.width),
223            None => self.layout.wrap_width,
224        };
225
226        paint_visual_text(
227            origin,
228            &self.layout.layout,
229            line_height,
230            align,
231            align_width,
232            &self.layout.visual_lines,
233            TextPaintPass::Background,
234            window,
235            cx,
236        )
237    }
238}
239
240impl InlineLayout {
241    /// Paints the background color of each text run in the inline layout.
242    ///
243    /// `origin` is the top-left corner of the layout's box. Call this before
244    /// [`InlineLayout::paint`].
245    pub fn paint_background(
246        &self,
247        origin: Point<Pixels>,
248        window: &mut Window,
249        cx: &mut App,
250    ) -> Result<()> {
251        paint_inline_layout(self, origin, TextPaintPass::Background, window, cx)
252    }
253
254    /// Paints the glyphs and underlines or strikethroughs of the inline layout.
255    ///
256    /// `origin` is the top-left corner of the layout's box. This method paints
257    /// only text. The atomic boxes in [`InlineLayout::boxes`] are not painted here.
258    pub fn paint(&self, origin: Point<Pixels>, window: &mut Window, cx: &mut App) -> Result<()> {
259        paint_inline_layout(self, origin, TextPaintPass::Foreground, window, cx)
260    }
261}
262
263#[derive(Clone, Copy, PartialEq)]
264enum TextPaintPass {
265    Background,
266    Foreground,
267}
268
269struct FragmentPaintContext<'a> {
270    line_origin: Point<Pixels>,
271    line_height: Pixels,
272    baseline_y: Pixels,
273    layout: &'a LineLayout,
274    pass: TextPaintPass,
275    text_system: &'a TextSystem,
276}
277
278fn paint_inline_layout(
279    inline: &InlineLayout,
280    origin: Point<Pixels>,
281    pass: TextPaintPass,
282    window: &mut Window,
283    cx: &mut App,
284) -> Result<()> {
285    if inline.lines.is_empty() {
286        return Ok(());
287    }
288
289    let text_system = cx.text_system().clone();
290    let placement = place_inline_layout(origin, inline.alignment_offset, window);
291    window.paint_layer(Bounds::new(placement.origin, inline.size), |window| {
292        for (line, visual_line) in inline.lines.iter().zip(&inline.layout.visual_lines) {
293            let line_origin = origin + line.origin + placement.delta;
294            paint_visual_line(
295                &inline.layout,
296                visual_line,
297                line_origin,
298                line.size.height,
299                line_origin.y + line.baseline,
300                pass,
301                &text_system,
302                window,
303            );
304        }
305
306        Ok(())
307    })
308}
309
310#[derive(Clone, Copy, Debug, PartialEq)]
311pub(crate) struct InlineLayoutPlacement {
312    pub(crate) origin: Point<Pixels>,
313    pub(crate) delta: Point<Pixels>,
314}
315
316pub(crate) fn place_inline_layout(
317    content_origin: Point<Pixels>,
318    alignment_offset: Pixels,
319    window: &Window,
320) -> InlineLayoutPlacement {
321    let anchor = content_origin + point(alignment_offset, Pixels::ZERO);
322    let placed_anchor = window.pixel_snap_point(anchor);
323    let delta = placed_anchor - anchor;
324
325    InlineLayoutPlacement {
326        origin: content_origin + delta,
327        delta,
328    }
329}
330
331fn paint_visual_line(
332    layout: &LineLayout,
333    visual_line: &VisualLine,
334    line_origin: Point<Pixels>,
335    line_height: Pixels,
336    baseline_y: Pixels,
337    pass: TextPaintPass,
338    text_system: &TextSystem,
339    window: &mut Window,
340) {
341    let context = FragmentPaintContext {
342        line_origin,
343        line_height,
344        baseline_y,
345        layout,
346        pass,
347        text_system,
348    };
349    for fragment in &layout.paint_fragments[visual_line.paint_fragment_range.clone()] {
350        paint_text_fragment(fragment, &context, window);
351    }
352}
353
354fn paint_text_fragment(
355    fragment: &PaintFragment,
356    context: &FragmentPaintContext<'_>,
357    window: &mut Window,
358) {
359    paint_fragment_decorations_at(
360        fragment,
361        context.line_origin,
362        context.line_height,
363        context.baseline_y,
364        context.layout,
365        window,
366        context.pass == TextPaintPass::Foreground,
367    );
368    if context.pass == TextPaintPass::Background {
369        return;
370    }
371
372    let max_glyph_size = context
373        .text_system
374        .bounding_box(fragment.font_id, fragment.font_size)
375        .size;
376    for glyph in &*fragment.glyphs {
377        let cull_origin = point(
378            context.line_origin.x + glyph.position.x,
379            context.line_origin.y,
380        );
381        if !Bounds::new(cull_origin, max_glyph_size).intersects(&window.content_mask().bounds) {
382            continue;
383        }
384
385        let glyph_origin = point(
386            context.line_origin.x + glyph.position.x,
387            context.baseline_y + glyph.position.y,
388        );
389        let result = if glyph.is_emoji {
390            window.paint_emoji_with_color(
391                glyph_origin,
392                fragment.font_id,
393                glyph.id,
394                fragment.font_size,
395                fragment.style.color,
396            )
397        } else {
398            window.paint_glyph(
399                glyph_origin,
400                fragment.font_id,
401                glyph.id,
402                fragment.font_size,
403                fragment.style.color,
404            )
405        };
406
407        if let Err(error) = result {
408            let error_idx = GLYPH_PAINT_ERRORS.fetch_add(1, Ordering::Relaxed);
409
410            if error_idx < MAX_LOGGED_GLYPH_PAINT_ERRORS {
411                log::error!(
412                    "failed to paint glyph {:?} from font {:?}: {error:#}",
413                    glyph.id,
414                    fragment.font_id
415                );
416            } else if error_idx == MAX_LOGGED_GLYPH_PAINT_ERRORS {
417                log::error!("suppressing further glyph paint errors");
418            }
419        }
420    }
421}
422
423fn paint_visual_text(
424    origin: Point<Pixels>,
425    layout: &LineLayout,
426    line_height: Pixels,
427    align: TextAlign,
428    align_width: Option<Pixels>,
429    visual_lines: &[VisualLine],
430    pass: TextPaintPass,
431    window: &mut Window,
432    cx: &mut App,
433) -> Result<()> {
434    if visual_lines.is_empty() {
435        return Ok(());
436    }
437
438    let line_bounds = visual_text_bounds(origin, line_height, align, align_width, visual_lines);
439    window.paint_layer(line_bounds, |window| {
440        let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
441        let text_system = cx.text_system().clone();
442
443        for (line_index, line) in visual_lines.iter().enumerate() {
444            let line_origin = point(
445                visual_line_origin_x(origin.x, align, align_width, line),
446                origin.y + line_index as f32 * line_height,
447            );
448            paint_visual_line(
449                layout,
450                line,
451                line_origin,
452                line_height,
453                line_origin.y + padding_top + layout.ascent,
454                pass,
455                &text_system,
456                window,
457            );
458        }
459
460        Ok(())
461    })
462}
463
464fn visual_text_bounds(
465    origin: Point<Pixels>,
466    line_height: Pixels,
467    align: TextAlign,
468    align_width: Option<Pixels>,
469    visual_lines: &[VisualLine],
470) -> Bounds<Pixels> {
471    let mut left = Pixels::MAX;
472    let mut right = Pixels::MIN;
473
474    for line in visual_lines {
475        let line_left = visual_line_origin_x(origin.x, align, align_width, line);
476        left = left.min(line_left);
477        right = right.max(line_left + line.advance_width);
478    }
479    Bounds::new(
480        point(left, origin.y),
481        size(right - left, line_height * visual_lines.len() as f32),
482    )
483}
484
485fn visual_line_origin_x(
486    origin_x: Pixels,
487    align: TextAlign,
488    align_width: Option<Pixels>,
489    line: &VisualLine,
490) -> Pixels {
491    line.offset
492        + aligned_visual_origin_x(
493            origin_x,
494            align_width.unwrap_or(line.advance_width),
495            line.advance_width,
496            align,
497            line.direction,
498        )
499}
500
501fn paint_fragment_decorations_at(
502    fragment: &PaintFragment,
503    line_origin: Point<Pixels>,
504    line_height: Pixels,
505    baseline_y: Pixels,
506    layout: &LineLayout,
507    window: &mut Window,
508    foreground: bool,
509) {
510    let range = line_origin.x + fragment.x_range.start..line_origin.x + fragment.x_range.end;
511    if foreground {
512        if let Some(mut underline) = fragment.style.underline {
513            underline.color = Some(underline.color.unwrap_or(fragment.style.color));
514            window.paint_underline(
515                point(
516                    range.start,
517                    baseline_y + fragment.underline_offset.unwrap_or(layout.descent * 0.618),
518                ),
519                range.end - range.start,
520                &underline,
521            );
522        }
523        if let Some(mut strike) = fragment.style.strikethrough {
524            strike.color = Some(strike.color.unwrap_or(fragment.style.color));
525            let strike_y = baseline_y
526                + fragment
527                    .strikethrough_offset
528                    .unwrap_or(-layout.ascent * 0.5);
529            window.paint_strikethrough(
530                point(range.start, strike_y),
531                range.end - range.start,
532                &strike,
533            );
534        }
535    } else if let Some(color) = fragment.style.background_color {
536        window.paint_quad(
537            PaintQuad::new(Bounds::new(
538                point(range.start, line_origin.y),
539                size(range.end - range.start, line_height),
540            ))
541            .background(color),
542        );
543    }
544}
545
546fn aligned_visual_origin_x(
547    origin_x: Pixels,
548    align_width: Pixels,
549    line_width: Pixels,
550    align: TextAlign,
551    direction: crate::ResolvedDirection,
552) -> Pixels {
553    match align {
554        TextAlign::Start if direction.is_rtl() => origin_x + align_width - line_width,
555        TextAlign::Start | TextAlign::Left => origin_x,
556        TextAlign::Center => (origin_x * 2.0 + align_width - line_width) / 2.0,
557        TextAlign::End if direction.is_rtl() => origin_x,
558        TextAlign::End | TextAlign::Right => origin_x + align_width - line_width,
559    }
560}