line.rs

  1use crate::{
  2    black, fill, point, px, size, App, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result,
  3    SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, WrapBoundary,
  4    WrappedLineLayout,
  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
 86/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
 87#[derive(Clone, Default, Debug, Deref, DerefMut)]
 88pub struct WrappedLine {
 89    #[deref]
 90    #[deref_mut]
 91    pub(crate) layout: Arc<WrappedLineLayout>,
 92    /// The text that was shaped for this line.
 93    pub text: SharedString,
 94    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 95}
 96
 97impl WrappedLine {
 98    /// The length of the underlying, unwrapped layout, in utf-8 bytes.
 99    #[allow(clippy::len_without_is_empty)]
100    pub fn len(&self) -> usize {
101        self.layout.len()
102    }
103
104    /// Paint this line of text to the window.
105    pub fn paint(
106        &self,
107        origin: Point<Pixels>,
108        line_height: Pixels,
109        align: TextAlign,
110        bounds: Option<Bounds<Pixels>>,
111        window: &mut Window,
112        cx: &mut App,
113    ) -> Result<()> {
114        let align_width = match bounds {
115            Some(bounds) => Some(bounds.size.width),
116            None => self.layout.wrap_width,
117        };
118
119        paint_line(
120            origin,
121            &self.layout.unwrapped_layout,
122            line_height,
123            align,
124            align_width,
125            &self.decoration_runs,
126            &self.wrap_boundaries,
127            window,
128            cx,
129        )?;
130
131        Ok(())
132    }
133}
134
135fn paint_line(
136    origin: Point<Pixels>,
137    layout: &LineLayout,
138    line_height: Pixels,
139    align: TextAlign,
140    align_width: Option<Pixels>,
141    decoration_runs: &[DecorationRun],
142    wrap_boundaries: &[WrapBoundary],
143    window: &mut Window,
144    cx: &mut App,
145) -> Result<()> {
146    let line_bounds = Bounds::new(
147        origin,
148        size(
149            layout.width,
150            line_height * (wrap_boundaries.len() as f32 + 1.),
151        ),
152    );
153    window.paint_layer(line_bounds, |window| {
154        let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
155        let baseline_offset = point(px(0.), padding_top + layout.ascent);
156        let mut decoration_runs = decoration_runs.iter();
157        let mut wraps = wrap_boundaries.iter().peekable();
158        let mut run_end = 0;
159        let mut color = black();
160        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
161        let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
162        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
163        let text_system = cx.text_system().clone();
164        let mut glyph_origin = point(
165            aligned_origin_x(
166                origin,
167                align_width.unwrap_or(layout.width),
168                px(0.0),
169                &align,
170                layout,
171                wraps.peek(),
172            ),
173            origin.y,
174        );
175        let mut prev_glyph_position = Point::default();
176        let mut max_glyph_size = size(px(0.), px(0.));
177        for (run_ix, run) in layout.runs.iter().enumerate() {
178            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
179
180            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
181                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
182
183                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
184                    wraps.next();
185                    if let Some((background_origin, background_color)) = current_background.as_mut()
186                    {
187                        if glyph_origin.x == background_origin.x {
188                            background_origin.x -= max_glyph_size.width.half()
189                        }
190                        window.paint_quad(fill(
191                            Bounds {
192                                origin: *background_origin,
193                                size: size(glyph_origin.x - background_origin.x, line_height),
194                            },
195                            *background_color,
196                        ));
197                        background_origin.x = origin.x;
198                        background_origin.y += line_height;
199                    }
200                    if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
201                        if glyph_origin.x == underline_origin.x {
202                            underline_origin.x -= max_glyph_size.width.half();
203                        };
204                        window.paint_underline(
205                            *underline_origin,
206                            glyph_origin.x - underline_origin.x,
207                            underline_style,
208                        );
209                        underline_origin.x = origin.x;
210                        underline_origin.y += line_height;
211                    }
212                    if let Some((strikethrough_origin, strikethrough_style)) =
213                        current_strikethrough.as_mut()
214                    {
215                        if glyph_origin.x == strikethrough_origin.x {
216                            strikethrough_origin.x -= max_glyph_size.width.half();
217                        };
218                        window.paint_strikethrough(
219                            *strikethrough_origin,
220                            glyph_origin.x - strikethrough_origin.x,
221                            strikethrough_style,
222                        );
223                        strikethrough_origin.x = origin.x;
224                        strikethrough_origin.y += line_height;
225                    }
226
227                    glyph_origin.x = aligned_origin_x(
228                        origin,
229                        align_width.unwrap_or(layout.width),
230                        glyph.position.x,
231                        &align,
232                        layout,
233                        wraps.peek(),
234                    );
235                    glyph_origin.y += line_height;
236                }
237                prev_glyph_position = glyph.position;
238
239                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
240                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
241                let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
242                if glyph.index >= run_end {
243                    let mut style_run = decoration_runs.next();
244
245                    // ignore style runs that apply to a partial glyph
246                    while let Some(run) = style_run {
247                        if glyph.index < run_end + (run.len as usize) {
248                            break;
249                        }
250                        run_end += run.len as usize;
251                        style_run = decoration_runs.next();
252                    }
253
254                    if let Some(style_run) = style_run {
255                        if let Some((_, background_color)) = &mut current_background {
256                            if style_run.background_color.as_ref() != Some(background_color) {
257                                finished_background = current_background.take();
258                            }
259                        }
260                        if let Some(run_background) = style_run.background_color {
261                            current_background.get_or_insert((
262                                point(glyph_origin.x, glyph_origin.y),
263                                run_background,
264                            ));
265                        }
266
267                        if let Some((_, underline_style)) = &mut current_underline {
268                            if style_run.underline.as_ref() != Some(underline_style) {
269                                finished_underline = current_underline.take();
270                            }
271                        }
272                        if let Some(run_underline) = style_run.underline.as_ref() {
273                            current_underline.get_or_insert((
274                                point(
275                                    glyph_origin.x,
276                                    glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
277                                ),
278                                UnderlineStyle {
279                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
280                                    thickness: run_underline.thickness,
281                                    wavy: run_underline.wavy,
282                                },
283                            ));
284                        }
285                        if let Some((_, strikethrough_style)) = &mut current_strikethrough {
286                            if style_run.strikethrough.as_ref() != Some(strikethrough_style) {
287                                finished_strikethrough = current_strikethrough.take();
288                            }
289                        }
290                        if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
291                            current_strikethrough.get_or_insert((
292                                point(
293                                    glyph_origin.x,
294                                    glyph_origin.y
295                                        + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
296                                ),
297                                StrikethroughStyle {
298                                    color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
299                                    thickness: run_strikethrough.thickness,
300                                },
301                            ));
302                        }
303
304                        run_end += style_run.len as usize;
305                        color = style_run.color;
306                    } else {
307                        run_end = layout.len;
308                        finished_background = current_background.take();
309                        finished_underline = current_underline.take();
310                        finished_strikethrough = current_strikethrough.take();
311                    }
312                }
313
314                if let Some((mut background_origin, background_color)) = finished_background {
315                    let mut width = glyph_origin.x - background_origin.x;
316                    if background_origin.x == glyph_origin.x {
317                        background_origin.x -= max_glyph_size.width.half();
318                    };
319                    window.paint_quad(fill(
320                        Bounds {
321                            origin: background_origin,
322                            size: size(width, line_height),
323                        },
324                        background_color,
325                    ));
326                }
327
328                if let Some((mut underline_origin, underline_style)) = finished_underline {
329                    if underline_origin.x == glyph_origin.x {
330                        underline_origin.x -= max_glyph_size.width.half();
331                    };
332                    window.paint_underline(
333                        underline_origin,
334                        glyph_origin.x - underline_origin.x,
335                        &underline_style,
336                    );
337                }
338
339                if let Some((mut strikethrough_origin, strikethrough_style)) =
340                    finished_strikethrough
341                {
342                    if strikethrough_origin.x == glyph_origin.x {
343                        strikethrough_origin.x -= max_glyph_size.width.half();
344                    };
345                    window.paint_strikethrough(
346                        strikethrough_origin,
347                        glyph_origin.x - strikethrough_origin.x,
348                        &strikethrough_style,
349                    );
350                }
351
352                let max_glyph_bounds = Bounds {
353                    origin: glyph_origin,
354                    size: max_glyph_size,
355                };
356
357                let content_mask = window.content_mask();
358                if max_glyph_bounds.intersects(&content_mask.bounds) {
359                    if glyph.is_emoji {
360                        window.paint_emoji(
361                            glyph_origin + baseline_offset,
362                            run.font_id,
363                            glyph.id,
364                            layout.font_size,
365                        )?;
366                    } else {
367                        window.paint_glyph(
368                            glyph_origin + baseline_offset,
369                            run.font_id,
370                            glyph.id,
371                            layout.font_size,
372                            color,
373                        )?;
374                    }
375                }
376            }
377        }
378
379        let mut last_line_end_x = origin.x + layout.width;
380        if let Some(boundary) = wrap_boundaries.last() {
381            let run = &layout.runs[boundary.run_ix];
382            let glyph = &run.glyphs[boundary.glyph_ix];
383            last_line_end_x -= glyph.position.x;
384        }
385
386        if let Some((mut background_origin, background_color)) = current_background.take() {
387            if last_line_end_x == background_origin.x {
388                background_origin.x -= max_glyph_size.width.half()
389            };
390            window.paint_quad(fill(
391                Bounds {
392                    origin: background_origin,
393                    size: size(last_line_end_x - background_origin.x, line_height),
394                },
395                background_color,
396            ));
397        }
398
399        if let Some((mut underline_start, underline_style)) = current_underline.take() {
400            if last_line_end_x == underline_start.x {
401                underline_start.x -= max_glyph_size.width.half()
402            };
403            window.paint_underline(
404                underline_start,
405                last_line_end_x - underline_start.x,
406                &underline_style,
407            );
408        }
409
410        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
411            if last_line_end_x == strikethrough_start.x {
412                strikethrough_start.x -= max_glyph_size.width.half()
413            };
414            window.paint_strikethrough(
415                strikethrough_start,
416                last_line_end_x - strikethrough_start.x,
417                &strikethrough_style,
418            );
419        }
420
421        Ok(())
422    })
423}
424
425fn aligned_origin_x(
426    origin: Point<Pixels>,
427    align_width: Pixels,
428    last_glyph_x: Pixels,
429    align: &TextAlign,
430    layout: &LineLayout,
431    wrap_boundary: Option<&&WrapBoundary>,
432) -> Pixels {
433    let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
434        layout.runs[*run_ix].glyphs[*glyph_ix].position.x
435    } else {
436        layout.width
437    };
438
439    let line_width = end_of_line - last_glyph_x;
440
441    match align {
442        TextAlign::Left => origin.x,
443        TextAlign::Center => (2.0 * origin.x + align_width - line_width) / 2.0,
444        TextAlign::Right => origin.x + align_width - line_width,
445    }
446}