line.rs

  1use crate::{
  2    black, fill, point, px, size, Bounds, ElementContext, Hsla, LineLayout, Pixels, Point, Result,
  3    SharedString, UnderlineStyle, 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
 25/// A line of text that has been shaped and decorated.
 26#[derive(Clone, Default, Debug, Deref, DerefMut)]
 27pub struct ShapedLine {
 28    #[deref]
 29    #[deref_mut]
 30    pub(crate) layout: Arc<LineLayout>,
 31    /// The text that was shaped for this line.
 32    pub text: SharedString,
 33    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 34}
 35
 36impl ShapedLine {
 37    /// The length of the line in utf-8 bytes.
 38    #[allow(clippy::len_without_is_empty)]
 39    pub fn len(&self) -> usize {
 40        self.layout.len
 41    }
 42
 43    /// Paint the line of text to the window.
 44    pub fn paint(
 45        &self,
 46        origin: Point<Pixels>,
 47        line_height: Pixels,
 48        cx: &mut ElementContext,
 49    ) -> Result<()> {
 50        paint_line(
 51            origin,
 52            &self.layout,
 53            line_height,
 54            &self.decoration_runs,
 55            &[],
 56            cx,
 57        )?;
 58
 59        Ok(())
 60    }
 61}
 62
 63/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
 64#[derive(Clone, Default, Debug, Deref, DerefMut)]
 65pub struct WrappedLine {
 66    #[deref]
 67    #[deref_mut]
 68    pub(crate) layout: Arc<WrappedLineLayout>,
 69    /// The text that was shaped for this line.
 70    pub text: SharedString,
 71    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 72}
 73
 74impl WrappedLine {
 75    /// The length of the underlying, unwrapped layout, in utf-8 bytes.
 76    #[allow(clippy::len_without_is_empty)]
 77    pub fn len(&self) -> usize {
 78        self.layout.len()
 79    }
 80
 81    /// Paint this line of text to the window.
 82    pub fn paint(
 83        &self,
 84        origin: Point<Pixels>,
 85        line_height: Pixels,
 86        cx: &mut ElementContext,
 87    ) -> Result<()> {
 88        paint_line(
 89            origin,
 90            &self.layout.unwrapped_layout,
 91            line_height,
 92            &self.decoration_runs,
 93            &self.wrap_boundaries,
 94            cx,
 95        )?;
 96
 97        Ok(())
 98    }
 99}
100
101fn paint_line(
102    origin: Point<Pixels>,
103    layout: &LineLayout,
104    line_height: Pixels,
105    decoration_runs: &[DecorationRun],
106    wrap_boundaries: &[WrapBoundary],
107    cx: &mut ElementContext<'_>,
108) -> Result<()> {
109    let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
110    let baseline_offset = point(px(0.), padding_top + layout.ascent);
111    let mut decoration_runs = decoration_runs.iter();
112    let mut wraps = wrap_boundaries.iter().peekable();
113    let mut run_end = 0;
114    let mut color = black();
115    let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
116    let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
117    let text_system = cx.text_system().clone();
118    let mut glyph_origin = origin;
119    let mut prev_glyph_position = Point::default();
120    for (run_ix, run) in layout.runs.iter().enumerate() {
121        let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
122
123        for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
124            glyph_origin.x += glyph.position.x - prev_glyph_position.x;
125
126            if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
127                wraps.next();
128                if let Some((background_origin, background_color)) = current_background.as_mut() {
129                    cx.paint_quad(fill(
130                        Bounds {
131                            origin: *background_origin,
132                            size: size(glyph_origin.x - background_origin.x, line_height),
133                        },
134                        *background_color,
135                    ));
136                    background_origin.x = origin.x;
137                    background_origin.y += line_height;
138                }
139                if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
140                    cx.paint_underline(
141                        *underline_origin,
142                        glyph_origin.x - underline_origin.x,
143                        underline_style,
144                    );
145                    underline_origin.x = origin.x;
146                    underline_origin.y += line_height;
147                }
148
149                glyph_origin.x = origin.x;
150                glyph_origin.y += line_height;
151            }
152            prev_glyph_position = glyph.position;
153
154            let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
155            let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
156            if glyph.index >= run_end {
157                if let Some(style_run) = decoration_runs.next() {
158                    if let Some((_, background_color)) = &mut current_background {
159                        if style_run.background_color.as_ref() != Some(background_color) {
160                            finished_background = current_background.take();
161                        }
162                    }
163                    if let Some(run_background) = style_run.background_color {
164                        current_background
165                            .get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background));
166                    }
167
168                    if let Some((_, underline_style)) = &mut current_underline {
169                        if style_run.underline.as_ref() != Some(underline_style) {
170                            finished_underline = current_underline.take();
171                        }
172                    }
173                    if let Some(run_underline) = style_run.underline.as_ref() {
174                        current_underline.get_or_insert((
175                            point(
176                                glyph_origin.x,
177                                glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
178                            ),
179                            UnderlineStyle {
180                                color: Some(run_underline.color.unwrap_or(style_run.color)),
181                                thickness: run_underline.thickness,
182                                wavy: run_underline.wavy,
183                            },
184                        ));
185                    }
186
187                    run_end += style_run.len as usize;
188                    color = style_run.color;
189                } else {
190                    run_end = layout.len;
191                    finished_background = current_background.take();
192                    finished_underline = current_underline.take();
193                }
194            }
195
196            if let Some((background_origin, background_color)) = finished_background {
197                cx.paint_quad(fill(
198                    Bounds {
199                        origin: background_origin,
200                        size: size(glyph_origin.x - background_origin.x, line_height),
201                    },
202                    background_color,
203                ));
204            }
205
206            if let Some((underline_origin, underline_style)) = finished_underline {
207                cx.paint_underline(
208                    underline_origin,
209                    glyph_origin.x - underline_origin.x,
210                    &underline_style,
211                );
212            }
213
214            let max_glyph_bounds = Bounds {
215                origin: glyph_origin,
216                size: max_glyph_size,
217            };
218
219            let content_mask = cx.content_mask();
220            if max_glyph_bounds.intersects(&content_mask.bounds) {
221                if glyph.is_emoji {
222                    cx.paint_emoji(
223                        glyph_origin + baseline_offset,
224                        run.font_id,
225                        glyph.id,
226                        layout.font_size,
227                    )?;
228                } else {
229                    cx.paint_glyph(
230                        glyph_origin + baseline_offset,
231                        run.font_id,
232                        glyph.id,
233                        layout.font_size,
234                        color,
235                    )?;
236                }
237            }
238        }
239    }
240
241    let mut last_line_end_x = origin.x + layout.width;
242    if let Some(boundary) = wrap_boundaries.last() {
243        let run = &layout.runs[boundary.run_ix];
244        let glyph = &run.glyphs[boundary.glyph_ix];
245        last_line_end_x -= glyph.position.x;
246    }
247
248    if let Some((background_origin, background_color)) = current_background.take() {
249        cx.paint_quad(fill(
250            Bounds {
251                origin: background_origin,
252                size: size(last_line_end_x - background_origin.x, line_height),
253            },
254            background_color,
255        ));
256    }
257
258    if let Some((underline_start, underline_style)) = current_underline.take() {
259        cx.paint_underline(
260            underline_start,
261            last_line_end_x - underline_start.x,
262            &underline_style,
263        );
264    }
265
266    Ok(())
267}