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