Skip to main content

gpui_core/text_system/
line.rs

1use crate::{
2    App, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result, SharedString, StrikethroughStyle,
3    TextAlign, TextShadow, UnderlineStyle, Window, WrapBoundary, WrappedLineLayout, black, fill,
4    point, px, size,
5};
6use derive_more::{Deref, DerefMut};
7use smallvec::SmallVec;
8use std::sync::Arc;
9
10/// Set the text decoration for a run of text.
11#[derive(Debug, Clone)]
12pub struct DecorationRun {
13    /// The length of the run in utf-8 bytes.
14    pub len: u32,
15
16    /// The color for this run
17    pub color: Hsla,
18
19    /// The background color for this run
20    pub background_color: Option<Hsla>,
21
22    /// The underline style for this run
23    pub underline: Option<UnderlineStyle>,
24
25    /// The strikethrough style for this run
26    pub strikethrough: Option<StrikethroughStyle>,
27}
28
29/// A line of text that has been shaped and decorated.
30#[derive(Clone, Default, Debug, Deref, DerefMut)]
31pub struct ShapedLine {
32    #[deref]
33    #[deref_mut]
34    pub(crate) layout: Arc<LineLayout>,
35    /// The text that was shaped for this line.
36    pub text: SharedString,
37    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
38}
39
40impl ShapedLine {
41    /// The length of the line in utf-8 bytes.
42    #[allow(clippy::len_without_is_empty)]
43    pub fn len(&self) -> usize {
44        self.layout.len
45    }
46
47    /// Override the len, useful if you're rendering text a
48    /// as text b (e.g. rendering invisibles).
49    pub fn with_len(mut self, len: usize) -> Self {
50        let layout = self.layout.as_ref();
51        self.layout = Arc::new(LineLayout {
52            font_size: layout.font_size,
53            width: layout.width,
54            ascent: layout.ascent,
55            descent: layout.descent,
56            runs: layout.runs.clone(),
57            len,
58        });
59        self
60    }
61
62    /// Paint the line of text to the window.
63    pub fn paint(
64        &self,
65        origin: Point<Pixels>,
66        line_height: Pixels,
67        window: &mut Window,
68        cx: &mut App,
69    ) -> Result<()> {
70        paint_line(
71            origin,
72            &self.layout,
73            line_height,
74            TextAlign::default(),
75            None,
76            &self.decoration_runs,
77            &[],
78            window,
79            cx,
80        )?;
81
82        Ok(())
83    }
84
85    /// Paint the background of the line to the window.
86    pub fn paint_background(
87        &self,
88        origin: Point<Pixels>,
89        line_height: Pixels,
90        window: &mut Window,
91        cx: &mut App,
92    ) -> Result<()> {
93        paint_line_background(
94            origin,
95            &self.layout,
96            line_height,
97            TextAlign::default(),
98            None,
99            &self.decoration_runs,
100            &[],
101            window,
102            cx,
103        )?;
104
105        Ok(())
106    }
107
108    /// Paint a shadow behind the text by rendering glyphs with offset and shadow color.
109    pub fn paint_shadow(
110        &self,
111        origin: Point<Pixels>,
112        line_height: Pixels,
113        shadow: &TextShadow,
114        window: &mut Window,
115        cx: &mut App,
116    ) -> Result<()> {
117        let shadow_runs: SmallVec<[DecorationRun; 32]> = self
118            .decoration_runs
119            .iter()
120            .map(|run| DecorationRun {
121                len: run.len,
122                color: shadow.color,
123                background_color: None,
124                underline: None,
125                strikethrough: None,
126            })
127            .collect();
128        let shadow_origin = point(origin.x + shadow.offset.x, origin.y + shadow.offset.y);
129        paint_line(
130            shadow_origin,
131            &self.layout,
132            line_height,
133            TextAlign::default(),
134            None,
135            &shadow_runs,
136            &[],
137            window,
138            cx,
139        )
140    }
141}
142
143impl LineLayout {
144    /// Paint this layout to the window, using the given decoration runs to color
145    /// glyphs and draw underlines and strikethroughs.
146    ///
147    /// This is a lower-level alternative to [`ShapedLine::paint`] for callers that
148    /// hold a bare layout and track decorations themselves.
149    pub fn paint(
150        &self,
151        origin: Point<Pixels>,
152        line_height: Pixels,
153        align: TextAlign,
154        align_width: Option<Pixels>,
155        decoration_runs: &[DecorationRun],
156        window: &mut Window,
157        cx: &mut App,
158    ) -> Result<()> {
159        paint_line(
160            origin,
161            self,
162            line_height,
163            align,
164            align_width,
165            decoration_runs,
166            &[],
167            window,
168            cx,
169        )
170    }
171
172    /// Paint the background of this layout to the window, using the given
173    /// decoration runs to determine background colors.
174    ///
175    /// This is a lower-level alternative to [`ShapedLine::paint_background`] for
176    /// callers that hold a bare layout and track decorations themselves.
177    pub fn paint_background(
178        &self,
179        origin: Point<Pixels>,
180        line_height: Pixels,
181        align: TextAlign,
182        align_width: Option<Pixels>,
183        decoration_runs: &[DecorationRun],
184        window: &mut Window,
185        cx: &mut App,
186    ) -> Result<()> {
187        paint_line_background(
188            origin,
189            self,
190            line_height,
191            align,
192            align_width,
193            decoration_runs,
194            &[],
195            window,
196            cx,
197        )
198    }
199}
200
201/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
202#[derive(Clone, Default, Debug, Deref, DerefMut)]
203pub struct WrappedLine {
204    #[deref]
205    #[deref_mut]
206    pub(crate) layout: Arc<WrappedLineLayout>,
207    /// The text that was shaped for this line.
208    pub text: SharedString,
209    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
210}
211
212impl WrappedLine {
213    /// The length of the underlying, unwrapped layout, in utf-8 bytes.
214    #[allow(clippy::len_without_is_empty)]
215    pub fn len(&self) -> usize {
216        self.layout.len()
217    }
218
219    /// Paint this line of text to the window.
220    pub fn paint(
221        &self,
222        origin: Point<Pixels>,
223        line_height: Pixels,
224        align: TextAlign,
225        bounds: Option<Bounds<Pixels>>,
226        window: &mut Window,
227        cx: &mut App,
228    ) -> Result<()> {
229        let align_width = match bounds {
230            Some(bounds) => Some(bounds.size.width),
231            None => self.layout.wrap_width,
232        };
233
234        paint_line(
235            origin,
236            &self.layout.unwrapped_layout,
237            line_height,
238            align,
239            align_width,
240            &self.decoration_runs,
241            &self.wrap_boundaries,
242            window,
243            cx,
244        )?;
245
246        Ok(())
247    }
248
249    /// Paint the background of line of text to the window.
250    pub fn paint_background(
251        &self,
252        origin: Point<Pixels>,
253        line_height: Pixels,
254        align: TextAlign,
255        bounds: Option<Bounds<Pixels>>,
256        window: &mut Window,
257        cx: &mut App,
258    ) -> Result<()> {
259        let align_width = match bounds {
260            Some(bounds) => Some(bounds.size.width),
261            None => self.layout.wrap_width,
262        };
263
264        paint_line_background(
265            origin,
266            &self.layout.unwrapped_layout,
267            line_height,
268            align,
269            align_width,
270            &self.decoration_runs,
271            &self.wrap_boundaries,
272            window,
273            cx,
274        )?;
275
276        Ok(())
277    }
278
279    /// Paint a shadow behind the text by rendering glyphs with offset and shadow color.
280    pub fn paint_shadow(
281        &self,
282        origin: Point<Pixels>,
283        line_height: Pixels,
284        align: TextAlign,
285        bounds: Option<Bounds<Pixels>>,
286        shadow: &TextShadow,
287        window: &mut Window,
288        cx: &mut App,
289    ) -> Result<()> {
290        let align_width = match bounds {
291            Some(bounds) => Some(bounds.size.width),
292            None => self.layout.wrap_width,
293        };
294        let shadow_runs: SmallVec<[DecorationRun; 32]> = self
295            .decoration_runs
296            .iter()
297            .map(|run| DecorationRun {
298                len: run.len,
299                color: shadow.color,
300                background_color: None,
301                underline: None,
302                strikethrough: None,
303            })
304            .collect();
305        let shadow_origin = point(origin.x + shadow.offset.x, origin.y + shadow.offset.y);
306        paint_line(
307            shadow_origin,
308            &self.layout.unwrapped_layout,
309            line_height,
310            align,
311            align_width,
312            &shadow_runs,
313            &self.wrap_boundaries,
314            window,
315            cx,
316        )
317    }
318}
319
320fn paint_line(
321    origin: Point<Pixels>,
322    layout: &LineLayout,
323    line_height: Pixels,
324    align: TextAlign,
325    align_width: Option<Pixels>,
326    decoration_runs: &[DecorationRun],
327    wrap_boundaries: &[WrapBoundary],
328    window: &mut Window,
329    cx: &mut App,
330) -> Result<()> {
331    let line_bounds = Bounds::new(
332        origin,
333        size(
334            layout.width,
335            line_height * (wrap_boundaries.len() as f32 + 1.),
336        ),
337    );
338    window.paint_layer(line_bounds, |window| {
339        let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
340        let baseline_offset = point(px(0.), padding_top + layout.ascent);
341        let mut decoration_runs = decoration_runs.iter();
342        let mut wraps = wrap_boundaries.iter().peekable();
343        let mut run_end = 0;
344        let mut color = black();
345        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
346        let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
347        let text_system = cx.text_system().clone();
348        let mut glyph_origin = point(
349            aligned_origin_x(
350                origin,
351                align_width.unwrap_or(layout.width),
352                px(0.0),
353                &align,
354                layout,
355                wraps.peek(),
356            ),
357            origin.y,
358        );
359        let mut prev_glyph_position = Point::default();
360        let mut max_glyph_size = size(px(0.), px(0.));
361        let mut first_glyph_x = origin.x;
362        for (run_ix, run) in layout.runs.iter().enumerate() {
363            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
364
365            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
366                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
367                if glyph_ix == 0 && run_ix == 0 {
368                    first_glyph_x = glyph_origin.x;
369                }
370
371                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
372                    wraps.next();
373                    if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
374                        if glyph_origin.x == underline_origin.x {
375                            underline_origin.x -= max_glyph_size.width.half();
376                        };
377                        window.paint_underline(
378                            *underline_origin,
379                            glyph_origin.x - underline_origin.x,
380                            underline_style,
381                        );
382                        if glyph.index < run_end {
383                            underline_origin.x = origin.x;
384                            underline_origin.y += line_height;
385                        } else {
386                            current_underline = None;
387                        }
388                    }
389                    if let Some((strikethrough_origin, strikethrough_style)) =
390                        current_strikethrough.as_mut()
391                    {
392                        if glyph_origin.x == strikethrough_origin.x {
393                            strikethrough_origin.x -= max_glyph_size.width.half();
394                        };
395                        window.paint_strikethrough(
396                            *strikethrough_origin,
397                            glyph_origin.x - strikethrough_origin.x,
398                            strikethrough_style,
399                        );
400                        if glyph.index < run_end {
401                            strikethrough_origin.x = origin.x;
402                            strikethrough_origin.y += line_height;
403                        } else {
404                            current_strikethrough = None;
405                        }
406                    }
407
408                    glyph_origin.x = aligned_origin_x(
409                        origin,
410                        align_width.unwrap_or(layout.width),
411                        glyph.position.x,
412                        &align,
413                        layout,
414                        wraps.peek(),
415                    );
416                    glyph_origin.y += line_height;
417                }
418                prev_glyph_position = glyph.position;
419
420                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
421                let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
422                if glyph.index >= run_end {
423                    let mut style_run = decoration_runs.next();
424
425                    // ignore style runs that apply to a partial glyph
426                    while let Some(run) = style_run {
427                        if glyph.index < run_end + (run.len as usize) {
428                            break;
429                        }
430                        run_end += run.len as usize;
431                        style_run = decoration_runs.next();
432                    }
433
434                    if let Some(style_run) = style_run {
435                        if let Some((_, underline_style)) = &mut current_underline
436                            && style_run.underline.as_ref() != Some(underline_style)
437                        {
438                            finished_underline = current_underline.take();
439                        }
440                        if let Some(run_underline) = style_run.underline.as_ref() {
441                            current_underline.get_or_insert((
442                                point(
443                                    glyph_origin.x,
444                                    glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
445                                ),
446                                UnderlineStyle {
447                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
448                                    thickness: run_underline.thickness,
449                                    wavy: run_underline.wavy,
450                                },
451                            ));
452                        }
453                        if let Some((_, strikethrough_style)) = &mut current_strikethrough
454                            && style_run.strikethrough.as_ref() != Some(strikethrough_style)
455                        {
456                            finished_strikethrough = current_strikethrough.take();
457                        }
458                        if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
459                            current_strikethrough.get_or_insert((
460                                point(
461                                    glyph_origin.x,
462                                    glyph_origin.y
463                                        + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
464                                ),
465                                StrikethroughStyle {
466                                    color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
467                                    thickness: run_strikethrough.thickness,
468                                },
469                            ));
470                        }
471
472                        run_end += style_run.len as usize;
473                        color = style_run.color;
474                    } else {
475                        run_end = layout.len;
476                        finished_underline = current_underline.take();
477                        finished_strikethrough = current_strikethrough.take();
478                    }
479                }
480
481                if let Some((mut underline_origin, underline_style)) = finished_underline {
482                    if underline_origin.x == glyph_origin.x {
483                        underline_origin.x -= max_glyph_size.width.half();
484                    };
485                    window.paint_underline(
486                        underline_origin,
487                        glyph_origin.x - underline_origin.x,
488                        &underline_style,
489                    );
490                }
491
492                if let Some((mut strikethrough_origin, strikethrough_style)) =
493                    finished_strikethrough
494                {
495                    if strikethrough_origin.x == glyph_origin.x {
496                        strikethrough_origin.x -= max_glyph_size.width.half();
497                    };
498                    window.paint_strikethrough(
499                        strikethrough_origin,
500                        glyph_origin.x - strikethrough_origin.x,
501                        &strikethrough_style,
502                    );
503                }
504
505                let max_glyph_bounds = Bounds {
506                    origin: glyph_origin,
507                    size: max_glyph_size,
508                };
509
510                let content_mask = window.content_mask();
511                if max_glyph_bounds.intersects(&content_mask.bounds) {
512                    if glyph.is_emoji {
513                        window.paint_emoji(
514                            glyph_origin + baseline_offset,
515                            run.font_id,
516                            glyph.id,
517                            layout.font_size,
518                        )?;
519                    } else {
520                        window.paint_glyph(
521                            glyph_origin + baseline_offset,
522                            run.font_id,
523                            glyph.id,
524                            layout.font_size,
525                            color,
526                        )?;
527                    }
528                }
529            }
530        }
531
532        let mut last_line_end_x = first_glyph_x + layout.width;
533        if let Some(boundary) = wrap_boundaries.last() {
534            let run = &layout.runs[boundary.run_ix];
535            let glyph = &run.glyphs[boundary.glyph_ix];
536            last_line_end_x -= glyph.position.x;
537        }
538
539        if let Some((mut underline_start, underline_style)) = current_underline.take() {
540            if last_line_end_x == underline_start.x {
541                underline_start.x -= max_glyph_size.width.half()
542            };
543            window.paint_underline(
544                underline_start,
545                last_line_end_x - underline_start.x,
546                &underline_style,
547            );
548        }
549
550        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
551            if last_line_end_x == strikethrough_start.x {
552                strikethrough_start.x -= max_glyph_size.width.half()
553            };
554            window.paint_strikethrough(
555                strikethrough_start,
556                last_line_end_x - strikethrough_start.x,
557                &strikethrough_style,
558            );
559        }
560
561        Ok(())
562    })
563}
564
565fn paint_line_background(
566    origin: Point<Pixels>,
567    layout: &LineLayout,
568    line_height: Pixels,
569    align: TextAlign,
570    align_width: Option<Pixels>,
571    decoration_runs: &[DecorationRun],
572    wrap_boundaries: &[WrapBoundary],
573    window: &mut Window,
574    cx: &mut App,
575) -> Result<()> {
576    let line_bounds = Bounds::new(
577        origin,
578        size(
579            layout.width,
580            line_height * (wrap_boundaries.len() as f32 + 1.),
581        ),
582    );
583    window.paint_layer(line_bounds, |window| {
584        let mut decoration_runs = decoration_runs.iter();
585        let mut wraps = wrap_boundaries.iter().peekable();
586        let mut run_end = 0;
587        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
588        let text_system = cx.text_system().clone();
589        let mut glyph_origin = point(
590            aligned_origin_x(
591                origin,
592                align_width.unwrap_or(layout.width),
593                px(0.0),
594                &align,
595                layout,
596                wraps.peek(),
597            ),
598            origin.y,
599        );
600        let mut prev_glyph_position = Point::default();
601        let mut max_glyph_size = size(px(0.), px(0.));
602        for (run_ix, run) in layout.runs.iter().enumerate() {
603            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
604
605            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
606                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
607
608                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
609                    wraps.next();
610                    if let Some((background_origin, background_color)) = current_background.as_mut()
611                    {
612                        if glyph_origin.x == background_origin.x {
613                            background_origin.x -= max_glyph_size.width.half()
614                        }
615                        window.paint_quad(fill(
616                            Bounds {
617                                origin: *background_origin,
618                                size: size(glyph_origin.x - background_origin.x, line_height),
619                            },
620                            *background_color,
621                        ));
622                        if glyph.index < run_end {
623                            background_origin.x = origin.x;
624                            background_origin.y += line_height;
625                        } else {
626                            current_background = None;
627                        }
628                    }
629
630                    glyph_origin.x = aligned_origin_x(
631                        origin,
632                        align_width.unwrap_or(layout.width),
633                        glyph.position.x,
634                        &align,
635                        layout,
636                        wraps.peek(),
637                    );
638                    glyph_origin.y += line_height;
639                }
640                prev_glyph_position = glyph.position;
641
642                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
643                if glyph.index >= run_end {
644                    let mut style_run = decoration_runs.next();
645
646                    // ignore style runs that apply to a partial glyph
647                    while let Some(run) = style_run {
648                        if glyph.index < run_end + (run.len as usize) {
649                            break;
650                        }
651                        run_end += run.len as usize;
652                        style_run = decoration_runs.next();
653                    }
654
655                    if let Some(style_run) = style_run {
656                        if let Some((_, background_color)) = &mut current_background
657                            && style_run.background_color.as_ref() != Some(background_color)
658                        {
659                            finished_background = current_background.take();
660                        }
661                        if let Some(run_background) = style_run.background_color {
662                            current_background.get_or_insert((
663                                point(glyph_origin.x, glyph_origin.y),
664                                run_background,
665                            ));
666                        }
667                        run_end += style_run.len as usize;
668                    } else {
669                        run_end = layout.len;
670                        finished_background = current_background.take();
671                    }
672                }
673
674                if let Some((mut background_origin, background_color)) = finished_background {
675                    let mut width = glyph_origin.x - background_origin.x;
676                    if background_origin.x == glyph_origin.x {
677                        background_origin.x -= max_glyph_size.width.half();
678                    };
679                    window.paint_quad(fill(
680                        Bounds {
681                            origin: background_origin,
682                            size: size(width, line_height),
683                        },
684                        background_color,
685                    ));
686                }
687            }
688        }
689
690        let mut last_line_end_x = origin.x + layout.width;
691        if let Some(boundary) = wrap_boundaries.last() {
692            let run = &layout.runs[boundary.run_ix];
693            let glyph = &run.glyphs[boundary.glyph_ix];
694            last_line_end_x -= glyph.position.x;
695        }
696
697        if let Some((mut background_origin, background_color)) = current_background.take() {
698            if last_line_end_x == background_origin.x {
699                background_origin.x -= max_glyph_size.width.half()
700            };
701            window.paint_quad(fill(
702                Bounds {
703                    origin: background_origin,
704                    size: size(last_line_end_x - background_origin.x, line_height),
705                },
706                background_color,
707            ));
708        }
709
710        Ok(())
711    })
712}
713
714fn aligned_origin_x(
715    origin: Point<Pixels>,
716    align_width: Pixels,
717    last_glyph_x: Pixels,
718    align: &TextAlign,
719    layout: &LineLayout,
720    wrap_boundary: Option<&&WrapBoundary>,
721) -> Pixels {
722    let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
723        layout.runs[*run_ix].glyphs[*glyph_ix].position.x
724    } else {
725        layout.width
726    };
727
728    let line_width = end_of_line - last_glyph_x;
729
730    match align {
731        TextAlign::Left => origin.x,
732        TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0,
733        TextAlign::Right => origin.x + align_width - line_width,
734    }
735}