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