line.rs

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