line.rs

  1use crate::{
  2    black, fill, point, px, size, App, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result,
  3    SharedString, StrikethroughStyle, UnderlineStyle, Window, WrapBoundary, WrappedLineLayout,
  4};
  5use derive_more::{Deref, DerefMut};
  6use smallvec::SmallVec;
  7use std::sync::Arc;
  8
  9/// Set the text decoration for a run of text.
 10#[derive(Debug, Clone)]
 11pub struct DecorationRun {
 12    /// The length of the run in utf-8 bytes.
 13    pub len: u32,
 14
 15    /// The color for this run
 16    pub color: Hsla,
 17
 18    /// The background color for this run
 19    pub background_color: Option<Hsla>,
 20
 21    /// The underline style for this run
 22    pub underline: Option<UnderlineStyle>,
 23
 24    /// The strikethrough style for this run
 25    pub strikethrough: Option<StrikethroughStyle>,
 26}
 27
 28/// A line of text that has been shaped and decorated.
 29#[derive(Clone, Default, Debug, Deref, DerefMut)]
 30pub struct ShapedLine {
 31    #[deref]
 32    #[deref_mut]
 33    pub(crate) layout: Arc<LineLayout>,
 34    /// The text that was shaped for this line.
 35    pub text: SharedString,
 36    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 37}
 38
 39impl ShapedLine {
 40    /// The length of the line in utf-8 bytes.
 41    #[allow(clippy::len_without_is_empty)]
 42    pub fn len(&self) -> usize {
 43        self.layout.len
 44    }
 45
 46    /// Override the len, useful if you're rendering text a
 47    /// as text b (e.g. rendering invisibles).
 48    pub fn with_len(mut self, len: usize) -> Self {
 49        let layout = self.layout.as_ref();
 50        self.layout = Arc::new(LineLayout {
 51            font_size: layout.font_size,
 52            width: layout.width,
 53            ascent: layout.ascent,
 54            descent: layout.descent,
 55            runs: layout.runs.clone(),
 56            len,
 57        });
 58        self
 59    }
 60
 61    /// Paint the line of text to the window.
 62    pub fn paint(
 63        &self,
 64        origin: Point<Pixels>,
 65        line_height: Pixels,
 66        window: &mut Window,
 67        cx: &mut App,
 68    ) -> Result<()> {
 69        paint_line(
 70            origin,
 71            &self.layout,
 72            line_height,
 73            &self.decoration_runs,
 74            &[],
 75            window,
 76            cx,
 77        )?;
 78
 79        Ok(())
 80    }
 81}
 82
 83/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
 84#[derive(Clone, Default, Debug, Deref, DerefMut)]
 85pub struct WrappedLine {
 86    #[deref]
 87    #[deref_mut]
 88    pub(crate) layout: Arc<WrappedLineLayout>,
 89    /// The text that was shaped for this line.
 90    pub text: SharedString,
 91    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 92}
 93
 94impl WrappedLine {
 95    /// The length of the underlying, unwrapped layout, in utf-8 bytes.
 96    #[allow(clippy::len_without_is_empty)]
 97    pub fn len(&self) -> usize {
 98        self.layout.len()
 99    }
100
101    /// Paint this line of text to the window.
102    pub fn paint(
103        &self,
104        origin: Point<Pixels>,
105        line_height: Pixels,
106        window: &mut Window,
107        cx: &mut App,
108    ) -> Result<()> {
109        paint_line(
110            origin,
111            &self.layout.unwrapped_layout,
112            line_height,
113            &self.decoration_runs,
114            &self.wrap_boundaries,
115            window,
116            cx,
117        )?;
118
119        Ok(())
120    }
121}
122
123fn paint_line(
124    origin: Point<Pixels>,
125    layout: &LineLayout,
126    line_height: Pixels,
127    decoration_runs: &[DecorationRun],
128    wrap_boundaries: &[WrapBoundary],
129    window: &mut Window,
130    cx: &mut App,
131) -> Result<()> {
132    let line_bounds = Bounds::new(
133        origin,
134        size(
135            layout.width,
136            line_height * (wrap_boundaries.len() as f32 + 1.),
137        ),
138    );
139    window.paint_layer(line_bounds, |window| {
140        let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
141        let baseline_offset = point(px(0.), padding_top + layout.ascent);
142        let mut decoration_runs = decoration_runs.iter();
143        let mut wraps = wrap_boundaries.iter().peekable();
144        let mut run_end = 0;
145        let mut color = black();
146        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
147        let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
148        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
149        let text_system = cx.text_system().clone();
150        let mut glyph_origin = origin;
151        let mut prev_glyph_position = Point::default();
152        let mut max_glyph_size = size(px(0.), px(0.));
153        for (run_ix, run) in layout.runs.iter().enumerate() {
154            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
155
156            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
157                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
158
159                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
160                    wraps.next();
161                    if let Some((background_origin, background_color)) = current_background.as_mut()
162                    {
163                        if glyph_origin.x == background_origin.x {
164                            background_origin.x -= max_glyph_size.width.half()
165                        }
166                        window.paint_quad(fill(
167                            Bounds {
168                                origin: *background_origin,
169                                size: size(glyph_origin.x - background_origin.x, line_height),
170                            },
171                            *background_color,
172                        ));
173                        background_origin.x = origin.x;
174                        background_origin.y += line_height;
175                    }
176                    if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
177                        if glyph_origin.x == underline_origin.x {
178                            underline_origin.x -= max_glyph_size.width.half();
179                        };
180                        window.paint_underline(
181                            *underline_origin,
182                            glyph_origin.x - underline_origin.x,
183                            underline_style,
184                        );
185                        underline_origin.x = origin.x;
186                        underline_origin.y += line_height;
187                    }
188                    if let Some((strikethrough_origin, strikethrough_style)) =
189                        current_strikethrough.as_mut()
190                    {
191                        if glyph_origin.x == strikethrough_origin.x {
192                            strikethrough_origin.x -= max_glyph_size.width.half();
193                        };
194                        window.paint_strikethrough(
195                            *strikethrough_origin,
196                            glyph_origin.x - strikethrough_origin.x,
197                            strikethrough_style,
198                        );
199                        strikethrough_origin.x = origin.x;
200                        strikethrough_origin.y += line_height;
201                    }
202
203                    glyph_origin.x = origin.x;
204                    glyph_origin.y += line_height;
205                }
206                prev_glyph_position = glyph.position;
207
208                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
209                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
210                let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
211                if glyph.index >= run_end {
212                    let mut style_run = decoration_runs.next();
213
214                    // ignore style runs that apply to a partial glyph
215                    while let Some(run) = style_run {
216                        if glyph.index < run_end + (run.len as usize) {
217                            break;
218                        }
219                        run_end += run.len as usize;
220                        style_run = decoration_runs.next();
221                    }
222
223                    if let Some(style_run) = style_run {
224                        if let Some((_, background_color)) = &mut current_background {
225                            if style_run.background_color.as_ref() != Some(background_color) {
226                                finished_background = current_background.take();
227                            }
228                        }
229                        if let Some(run_background) = style_run.background_color {
230                            current_background.get_or_insert((
231                                point(glyph_origin.x, glyph_origin.y),
232                                run_background,
233                            ));
234                        }
235
236                        if let Some((_, underline_style)) = &mut current_underline {
237                            if style_run.underline.as_ref() != Some(underline_style) {
238                                finished_underline = current_underline.take();
239                            }
240                        }
241                        if let Some(run_underline) = style_run.underline.as_ref() {
242                            current_underline.get_or_insert((
243                                point(
244                                    glyph_origin.x,
245                                    glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
246                                ),
247                                UnderlineStyle {
248                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
249                                    thickness: run_underline.thickness,
250                                    wavy: run_underline.wavy,
251                                },
252                            ));
253                        }
254                        if let Some((_, strikethrough_style)) = &mut current_strikethrough {
255                            if style_run.strikethrough.as_ref() != Some(strikethrough_style) {
256                                finished_strikethrough = current_strikethrough.take();
257                            }
258                        }
259                        if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
260                            current_strikethrough.get_or_insert((
261                                point(
262                                    glyph_origin.x,
263                                    glyph_origin.y
264                                        + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
265                                ),
266                                StrikethroughStyle {
267                                    color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
268                                    thickness: run_strikethrough.thickness,
269                                },
270                            ));
271                        }
272
273                        run_end += style_run.len as usize;
274                        color = style_run.color;
275                    } else {
276                        run_end = layout.len;
277                        finished_background = current_background.take();
278                        finished_underline = current_underline.take();
279                        finished_strikethrough = current_strikethrough.take();
280                    }
281                }
282
283                if let Some((mut background_origin, background_color)) = finished_background {
284                    let mut width = glyph_origin.x - background_origin.x;
285                    if background_origin.x == glyph_origin.x {
286                        background_origin.x -= max_glyph_size.width.half();
287                    };
288                    window.paint_quad(fill(
289                        Bounds {
290                            origin: background_origin,
291                            size: size(width, line_height),
292                        },
293                        background_color,
294                    ));
295                }
296
297                if let Some((mut underline_origin, underline_style)) = finished_underline {
298                    if underline_origin.x == glyph_origin.x {
299                        underline_origin.x -= max_glyph_size.width.half();
300                    };
301                    window.paint_underline(
302                        underline_origin,
303                        glyph_origin.x - underline_origin.x,
304                        &underline_style,
305                    );
306                }
307
308                if let Some((mut strikethrough_origin, strikethrough_style)) =
309                    finished_strikethrough
310                {
311                    if strikethrough_origin.x == glyph_origin.x {
312                        strikethrough_origin.x -= max_glyph_size.width.half();
313                    };
314                    window.paint_strikethrough(
315                        strikethrough_origin,
316                        glyph_origin.x - strikethrough_origin.x,
317                        &strikethrough_style,
318                    );
319                }
320
321                let max_glyph_bounds = Bounds {
322                    origin: glyph_origin,
323                    size: max_glyph_size,
324                };
325
326                let content_mask = window.content_mask();
327                if max_glyph_bounds.intersects(&content_mask.bounds) {
328                    if glyph.is_emoji {
329                        window.paint_emoji(
330                            glyph_origin + baseline_offset,
331                            run.font_id,
332                            glyph.id,
333                            layout.font_size,
334                        )?;
335                    } else {
336                        window.paint_glyph(
337                            glyph_origin + baseline_offset,
338                            run.font_id,
339                            glyph.id,
340                            layout.font_size,
341                            color,
342                        )?;
343                    }
344                }
345            }
346        }
347
348        let mut last_line_end_x = origin.x + layout.width;
349        if let Some(boundary) = wrap_boundaries.last() {
350            let run = &layout.runs[boundary.run_ix];
351            let glyph = &run.glyphs[boundary.glyph_ix];
352            last_line_end_x -= glyph.position.x;
353        }
354
355        if let Some((mut background_origin, background_color)) = current_background.take() {
356            if last_line_end_x == background_origin.x {
357                background_origin.x -= max_glyph_size.width.half()
358            };
359            window.paint_quad(fill(
360                Bounds {
361                    origin: background_origin,
362                    size: size(last_line_end_x - background_origin.x, line_height),
363                },
364                background_color,
365            ));
366        }
367
368        if let Some((mut underline_start, underline_style)) = current_underline.take() {
369            if last_line_end_x == underline_start.x {
370                underline_start.x -= max_glyph_size.width.half()
371            };
372            window.paint_underline(
373                underline_start,
374                last_line_end_x - underline_start.x,
375                &underline_style,
376            );
377        }
378
379        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
380            if last_line_end_x == strikethrough_start.x {
381                strikethrough_start.x -= max_glyph_size.width.half()
382            };
383            window.paint_strikethrough(
384                strikethrough_start,
385                last_line_end_x - strikethrough_start.x,
386                &strikethrough_style,
387            );
388        }
389
390        Ok(())
391    })
392}