Skip to main content

i_slint_core/textlayout/sharedparley/
draw.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Painting a [`Layout`] through a [`GlyphRenderer`]: glyph runs, decorations, inline-code
5//! capsules, and the clip-based recoloring of selected glyphs.
6
7use super::layout::ElisionCut;
8use super::selection::{RunCoverage, SelectionSpan, run_coverage};
9use super::shaping::{Brush, TextParagraph};
10use super::*;
11
12/// Outline drawn around a rectangle filled via [`GlyphRenderer::fill_rectangle`].
13#[derive(Clone)]
14pub struct RectangleBorder<Brush> {
15    pub brush: Brush,
16    pub width: PhysicalLength,
17}
18
19/// Trait used for drawing text and text input elements with parley, where parley does the
20/// shaping and positioning, and the renderer is responsible for drawing just the glyphs.
21pub trait GlyphRenderer: crate::item_rendering::ItemRenderer {
22    /// A renderer-specific type for a brush used for fill and stroke of glyphs.
23    type PlatformBrush: Clone;
24
25    /// Returns the brush to be used for filling text.
26    fn platform_text_fill_brush(
27        &mut self,
28        brush: crate::Brush,
29        size: LogicalSize,
30    ) -> Option<Self::PlatformBrush>;
31
32    /// Returns a brush that's a solid fill of the specified color.
33    fn platform_brush_for_color(&mut self, color: &Color) -> Option<Self::PlatformBrush>;
34
35    /// Returns the brush to be used for stroking text.
36    fn platform_text_stroke_brush(
37        &mut self,
38        brush: crate::Brush,
39        physical_stroke_width: f32,
40        size: LogicalSize,
41    ) -> Option<Self::PlatformBrush>;
42
43    /// Draws the glyphs provided by glyphs_it with the specified font, font_size, and brush at the
44    /// given y offset. The `normalized_coords` are F2Dot14 values in fvar axis order for variable
45    /// font rendering. The `synthesis` contains design-space variation settings and faux
46    /// bold/italic hints from fontique.
47    fn draw_glyph_run(
48        &mut self,
49        font: &parley::FontData,
50        font_size: PhysicalLength,
51        normalized_coords: &[i16],
52        synthesis: &fontique::Synthesis,
53        brush: Self::PlatformBrush,
54        y_offset: PhysicalLength,
55        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
56    );
57
58    /// Convenience wrapper around `fill_rectangle` that resolves `color` to a platform
59    /// brush and fills `physical_rect` with sharp corners and no outline.
60    fn fill_rectangle_with_color(&mut self, physical_rect: PhysicalRect, color: Color) {
61        if let Some(platform_brush) = self.platform_brush_for_color(&color) {
62            self.fill_rectangle(physical_rect, platform_brush, PhysicalLength::zero(), None);
63        }
64    }
65
66    /// Fills `physical_rect` with `brush`, optionally rounding the corners by `radius`
67    /// and outlining it with `border`. Passing a zero `radius` produces sharp corners;
68    /// passing `None` for `border` skips the outline.
69    fn fill_rectangle(
70        &mut self,
71        physical_rect: PhysicalRect,
72        brush: Self::PlatformBrush,
73        radius: PhysicalLength,
74        border: Option<RectangleBorder<Self::PlatformBrush>>,
75    );
76}
77
78/// The vertical extent the renderer clip lets anything be drawn in, as a physical y range in the
79/// current item's coordinates. A conservative superset of what is visible -- see
80/// [`crate::item_rendering::ItemRenderer::get_current_clip`] -- so it is a safe bound for
81/// skipping draw work, never for layout decisions.
82pub(super) fn visible_band(item_renderer: &impl GlyphRenderer) -> Range<PhysicalLength> {
83    let scale_factor = item_renderer.scale_factor();
84    let clip = item_renderer.get_current_clip();
85    let top = clip.origin.y_length() * scale_factor;
86    top..(top + clip.height_length() * scale_factor)
87}
88
89/// The horizontal counterpart of [`visible_band`].
90pub(super) fn visible_x_range(item_renderer: &impl GlyphRenderer) -> Range<PhysicalLength> {
91    let scale_factor = item_renderer.scale_factor();
92    let x_range = item_renderer.get_current_clip().x_length_range();
93    (x_range.start * scale_factor)..(x_range.end * scale_factor)
94}
95
96impl TextParagraph {
97    #[allow(clippy::too_many_arguments)]
98    fn draw<R: GlyphRenderer>(
99        &self,
100        layout: &Layout,
101        paragraph_index: usize,
102        visible_extent: Option<ElisionCut>,
103        visible_band: &Range<PhysicalLength>,
104        // `None` when eliding.
105        visible_x_range: Option<&Range<PhysicalLength>>,
106        item_renderer: &mut R,
107        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
108        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
109        default_text_color: Color,
110        selection: Option<&SelectionRendering<'_, R>>,
111    ) {
112        let para_y = layout.y_offset + self.y;
113
114        let line_count = self.layout.lines().len();
115
116        // For `overflow: elide` with a height limit (`overflow: clip` applies a hard pixel clip
117        // instead) and for `max-lines`, `visible_extent` decides -- across all paragraphs -- the
118        // last line to keep and where the vertical-truncation ellipsis goes. Translate it to this
119        // paragraph. `last_drawn` is the deepest line of this paragraph that we draw; it carries
120        // the horizontal ellipsis when it overflows the width. `vertical_truncation` marks the
121        // single global last kept line that must also show an ellipsis when lines below it were
122        // dropped.
123        let (last_drawn, vertical_truncation) = match visible_extent {
124            // Entirely below the kept block: drop the paragraph (don't redraw a stray first line,
125            // and don't paint inline-code backgrounds under text that isn't rendered).
126            Some(cut) if paragraph_index > cut.last_paragraph => return,
127            // The paragraph where the cut falls: stop at the global last kept line.
128            Some(cut) if paragraph_index == cut.last_paragraph => {
129                (cut.last_line, cut.needs_ellipsis)
130            }
131            // A paragraph fully above the cut, or no cut at all: draw every line that fits
132            // the box; the last visual line still elides horizontally when it is too wide.
133            _ => (line_count.saturating_sub(1), false),
134        };
135
136        self.draw_inline_code_backgrounds(item_renderer, para_y, default_text_color, last_drawn);
137
138        for (index, line) in self.layout.lines().enumerate() {
139            // Stop once we are past the last kept line of the last kept paragraph.
140            if index > last_drawn {
141                break;
142            }
143            let metrics = line.metrics();
144
145            // Skip lines that can't reach the visible band. Ink may overhang the line's metrics
146            // box (stacked diacritics, swashes), so pad by one line height on each side before
147            // excluding -- the pad scales with the line itself. Lines are in block order, so the
148            // first line past the band ends the walk.
149            let line_height =
150                PhysicalLength::new(metrics.block_max_coord - metrics.block_min_coord);
151            if para_y + PhysicalLength::new(metrics.block_max_coord) + line_height
152                < visible_band.start
153            {
154                continue;
155            }
156            if para_y + PhysicalLength::new(metrics.block_min_coord) - line_height
157                > visible_band.end
158            {
159                break;
160            }
161
162            // The kept line is always drawn, even when it slightly exceeds the box (#12197); other
163            // lines are kept only while they fall within the box, taking vertical alignment into
164            // account (bottom/center alignment clips lines off the top, not the bottom).
165            let last_line = index == last_drawn;
166            if !last_line
167                && !layout.paragraph_line_within_box(
168                    self,
169                    metrics.block_min_coord,
170                    metrics.block_max_coord,
171                )
172            {
173                continue;
174            }
175            // The last drawn line should show an ellipsis if real lines below it were dropped for
176            // the height, even when it fits the width.
177            let vertically_truncated = last_line && vertical_truncation;
178            let line_spans =
179                selection.map(|selection| selection.spans.for_line(paragraph_index, index));
180            // Padded for ink overhanging the advance, like the vertical filtering of lines.
181            let padded_x_range = visible_x_range
182                .map(|x_range| (x_range.start - line_height)..(x_range.end + line_height));
183            for item in line.items() {
184                match item {
185                    parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
186                        let mut glyph_x_range = None;
187                        if let Some(x_range) = &padded_x_range {
188                            let run_start = PhysicalLength::new(glyph_run.offset());
189                            let run_end =
190                                PhysicalLength::new(glyph_run.offset() + glyph_run.advance());
191                            if run_end < x_range.start || run_start > x_range.end {
192                                continue;
193                            }
194                            if run_start < x_range.start || run_end > x_range.end {
195                                glyph_x_range = Some(x_range);
196                            }
197                        }
198                        let ellipsis = if last_line {
199                            let (truncated_glyphs, ellipsis) = layout.glyphs_with_elision(
200                                &glyph_run,
201                                vertically_truncated,
202                                metrics.trailing_whitespace,
203                            );
204
205                            Self::draw_glyph_run_with_selection(
206                                &glyph_run,
207                                item_renderer,
208                                default_fill_brush,
209                                default_stroke_brush,
210                                para_y,
211                                glyph_x_range,
212                                &mut truncated_glyphs.into_iter(),
213                                selection.map(|selection| &selection.foreground),
214                                line_spans.unwrap_or_default(),
215                            );
216                            ellipsis
217                        } else {
218                            Self::draw_glyph_run_with_selection(
219                                &glyph_run,
220                                item_renderer,
221                                default_fill_brush,
222                                default_stroke_brush,
223                                para_y,
224                                glyph_x_range,
225                                &mut glyph_run.positioned_glyphs(),
226                                selection.map(|selection| &selection.foreground),
227                                line_spans.unwrap_or_default(),
228                            );
229                            None
230                        };
231
232                        if let Some((ellipsis_glyph, ellipsis_font, font_size)) = ellipsis {
233                            let run = glyph_run.run();
234                            item_renderer.draw_glyph_run(
235                                &ellipsis_font,
236                                font_size,
237                                run.normalized_coords(),
238                                &run.synthesis(),
239                                default_fill_brush.clone(),
240                                para_y,
241                                &mut core::iter::once(ellipsis_glyph),
242                            );
243                        }
244                    }
245                    parley::PositionedLayoutItem::InlineBox(_inline_box) => {}
246                };
247            }
248        }
249    }
250
251    /// Paints a translucent rounded capsule under every glyph run that lies inside one of
252    /// this paragraph's `Style::Code` ranges. Capsule colors are derived from the luminance
253    /// of `default_text_color`, so light and dark themes both get a sensible default
254    /// without any user-facing styling property.
255    fn draw_inline_code_backgrounds<R: GlyphRenderer>(
256        &self,
257        item_renderer: &mut R,
258        para_y: PhysicalLength,
259        default_text_color: Color,
260        last_drawn: usize,
261    ) {
262        if self.code_ranges.is_empty() {
263            return;
264        }
265
266        // Neutral gray fill (low alpha) on both themes — contrast against the page
267        // background carries the "this is code" cue. The border picks up the same hue
268        // but a higher alpha so the rounded outline stays visible against the fill.
269        // Pick brighter values on dark backgrounds (luminance of the text gives us
270        // that signal without poking at the window background).
271        let fg_luminance = 0.299 * default_text_color.red() as f32
272            + 0.587 * default_text_color.green() as f32
273            + 0.114 * default_text_color.blue() as f32;
274        let fill = Color::from_argb_u8(28, 128, 128, 128);
275        let border = if fg_luminance > 140.0 {
276            Color::from_argb_u8(88, 170, 170, 170)
277        } else {
278            Color::from_argb_u8(56, 128, 128, 128)
279        };
280        // Border width and radius bounds are logical so that the capsule looks the
281        // same at every DPI; the part of the radius derived from the capsule height
282        // already scales with the (physical) font size.
283        const BORDER_WIDTH: LogicalLength = LogicalLength::new(1.0);
284        const MIN_RADIUS: LogicalLength = LogicalLength::new(2.0);
285        const MAX_RADIUS: LogicalLength = LogicalLength::new(5.0);
286        // A touch of vertical padding above and below the cap-height / descender band
287        // so the capsule edge doesn't sit flush against tall glyphs.
288        const VERTICAL_PADDING_RATIO: f32 = 0.15;
289
290        let scale_factor = item_renderer.scale_factor();
291        let border_width = BORDER_WIDTH * scale_factor;
292
293        // Capsules only under lines that are drawn: lines past the visible-extent cut
294        // (`overflow: elide` height limit or `max-lines`) don't render their glyphs either.
295        for line in self.layout.lines().take(last_drawn + 1) {
296            for item in line.items() {
297                let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else {
298                    continue;
299                };
300                let run = glyph_run.run();
301                let run_range = run.text_range();
302                if run_range.is_empty() {
303                    continue;
304                }
305                // `Style::Code` pushes its own FontFamily + FontSize, which forces a
306                // run boundary, so a code run is always fully contained in one of the
307                // recorded ranges — a single containment check is enough.
308                let is_code = self
309                    .code_ranges
310                    .iter()
311                    .any(|cr| cr.start <= run_range.start && run_range.end <= cr.end);
312                if !is_code {
313                    continue;
314                }
315
316                let metrics = run.metrics();
317                let ascent = metrics.ascent;
318                let descent = metrics.descent;
319                let cap_height = metrics.cap_height.unwrap_or(ascent * 0.72);
320
321                // Center the capsule on the midpoint between cap-top and a shallow
322                // approximation of the descender bottom (roughly where parens, commas
323                // and dots reach). This gives equal visible padding above and below
324                // for typical code text (which has caps but rarely real descenders).
325                let upper_extent = cap_height;
326                let lower_extent = descent * 0.4;
327                let center = glyph_run.baseline() + (lower_extent - upper_extent) / 2.0;
328                let inner_half_height = (upper_extent + lower_extent) / 2.0;
329                let extra_padding = ascent * VERTICAL_PADDING_RATIO;
330                let half_height = inner_half_height + extra_padding;
331                let bg_height = (half_height * 2.0).max(1.0);
332                let bg_top = center - half_height;
333
334                // Width hugs the glyphs tightly — `glyph_run.advance()` is exactly
335                // the horizontal extent of the rendered run. The underlying text is
336                // not modified, so selection, hit-testing and copy/paste keep working
337                // on the underlying characters.
338                let bg_width = glyph_run.advance().max(0.0);
339                if bg_width <= 0.0 {
340                    continue;
341                }
342                let bg_left = glyph_run.offset();
343
344                let bg_rect = PhysicalRect::new(
345                    PhysicalPoint::from_lengths(
346                        PhysicalLength::new(bg_left),
347                        PhysicalLength::new(bg_top) + para_y,
348                    ),
349                    PhysicalSize::new(bg_width, bg_height),
350                );
351                let radius = PhysicalLength::new(bg_height * 0.22)
352                    .max(MIN_RADIUS * scale_factor)
353                    .min(MAX_RADIUS * scale_factor);
354                let Some(fill_brush) = item_renderer.platform_brush_for_color(&fill) else {
355                    continue;
356                };
357                let border_brush = item_renderer
358                    .platform_brush_for_color(&border)
359                    .map(|brush| RectangleBorder { brush, width: border_width });
360                item_renderer.fill_rectangle(bg_rect, fill_brush, radius, border_brush);
361            }
362        }
363    }
364
365    /// Draws one glyph run, splitting it where the selection starts or ends inside it.
366    ///
367    /// The overwhelmingly common cases -- a run that is entirely selected or entirely unselected
368    /// -- draw exactly once with no clip, so an enormous selection costs no more than a tiny one.
369    /// Only the at most two runs per selection edge that actually straddle a boundary are drawn
370    /// twice against a clip, and that is precisely where a ligature has to be cut in half.
371    #[allow(clippy::too_many_arguments)]
372    fn draw_glyph_run_with_selection<R: GlyphRenderer>(
373        glyph_run: &parley::layout::GlyphRun<Brush>,
374        item_renderer: &mut R,
375        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
376        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
377        para_y: PhysicalLength,
378        // A uniform `no-wrap` line is a single run, so culling whole runs is not enough.
379        visible_x_range: Option<&Range<PhysicalLength>>,
380        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
381        // The selection foreground, and the spans it covers on this run's line. Both empty when
382        // there is no selection, which `run_coverage` reports as `Unselected`.
383        selection_brush: Option<&<R as GlyphRenderer>::PlatformBrush>,
384        line_spans: &[SelectionSpan],
385    ) {
386        let run_x = glyph_run.offset()..glyph_run.offset() + glyph_run.advance();
387
388        // Bidirectional text reorders glyphs within a run, so this filters rather than truncating.
389        let x_range = visible_x_range.cloned();
390        let mut glyphs_it = glyphs_it.filter(move |glyph| {
391            x_range.as_ref().is_none_or(|x_range| {
392                let start = PhysicalLength::new(glyph.x);
393                let end = PhysicalLength::new(glyph.x + glyph.advance);
394                end >= x_range.start && start <= x_range.end
395            })
396        });
397        let glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph> = &mut glyphs_it;
398
399        match run_coverage(&run_x, line_spans) {
400            RunCoverage::Unselected => Self::draw_glyph_run(
401                glyph_run,
402                item_renderer,
403                default_fill_brush,
404                default_stroke_brush,
405                para_y,
406                glyphs_it,
407                None,
408            ),
409            RunCoverage::Full => Self::draw_glyph_run(
410                glyph_run,
411                item_renderer,
412                default_fill_brush,
413                default_stroke_brush,
414                para_y,
415                glyphs_it,
416                selection_brush,
417            ),
418            RunCoverage::Partial => {
419                // The run has to be rasterized once per segment, so the glyphs can't stay behind
420                // a one-shot iterator.
421                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
422
423                // Walk the run left to right, alternating unselected and selected segments. This
424                // relies on the spans being ascending in x, which [`SelectionSpans`] guarantees.
425                let mut x = run_x.start;
426                for span in line_spans {
427                    let span_x = span.x();
428                    if span_x.end <= run_x.start {
429                        continue;
430                    }
431                    if span_x.start >= run_x.end {
432                        break;
433                    }
434                    let start = span_x.start.max(run_x.start);
435                    let end = span_x.end.min(run_x.end);
436                    for (segment, brush) in [(x..start, None), (start..end, selection_brush)] {
437                        Self::draw_glyph_run_segment(
438                            glyph_run,
439                            item_renderer,
440                            default_fill_brush,
441                            default_stroke_brush,
442                            para_y,
443                            &glyphs,
444                            segment,
445                            brush,
446                        );
447                    }
448                    x = end;
449                }
450                Self::draw_glyph_run_segment(
451                    glyph_run,
452                    item_renderer,
453                    default_fill_brush,
454                    default_stroke_brush,
455                    para_y,
456                    &glyphs,
457                    x..run_x.end,
458                    None,
459                );
460            }
461        }
462    }
463
464    /// Draws `glyphs` clipped to the horizontal band `x`, so that a glyph straddling the band's
465    /// edge is cut rather than recolored as a whole.
466    fn draw_glyph_run_segment<R: GlyphRenderer>(
467        glyph_run: &parley::layout::GlyphRun<Brush>,
468        item_renderer: &mut R,
469        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
470        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
471        para_y: PhysicalLength,
472        glyphs: &[parley::layout::Glyph],
473        x: Range<f32>,
474        override_fill_brush: Option<&<R as GlyphRenderer>::PlatformBrush>,
475    ) {
476        if x.end <= x.start {
477            return;
478        }
479
480        item_renderer.save_state();
481
482        // Clip horizontally only: the vertical extent stays whatever is already in effect, so
483        // accents and descenders reaching outside the line box are never sheared off.
484        let scale_factor = item_renderer.scale_factor();
485        let current_clip = item_renderer.get_current_clip();
486        let render = item_renderer.combine_clip(
487            LogicalRect::new(
488                LogicalPoint::from_lengths(
489                    PhysicalLength::new(x.start) / scale_factor,
490                    current_clip.origin.y_length(),
491                ),
492                LogicalSize::from_lengths(
493                    PhysicalLength::new(x.end - x.start) / scale_factor,
494                    current_clip.height_length(),
495                ),
496            ),
497            LogicalBorderRadius::zero(),
498        );
499
500        if render {
501            Self::draw_glyph_run(
502                glyph_run,
503                item_renderer,
504                default_fill_brush,
505                default_stroke_brush,
506                para_y,
507                &mut glyphs.iter().cloned(),
508                override_fill_brush,
509            );
510        }
511
512        item_renderer.restore_state();
513    }
514
515    fn draw_glyph_run<R: GlyphRenderer>(
516        glyph_run: &parley::layout::GlyphRun<Brush>,
517        item_renderer: &mut R,
518        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
519        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
520        para_y: PhysicalLength,
521        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
522        // Forced fill for selected glyphs, overriding the run's own brush.
523        override_fill_brush: Option<&<R as GlyphRenderer>::PlatformBrush>,
524    ) {
525        let run = glyph_run.run();
526        let normalized_coords = run.normalized_coords();
527        let synthesis = run.synthesis();
528        let brush = &glyph_run.style().brush;
529
530        let (fill_brush, stroke_style) = match override_fill_brush {
531            // Selection wins over a `Style::Color` span and over a link color: text under the
532            // highlight has to stay legible against the selection background.
533            Some(selection_brush) => (selection_brush.clone(), &None),
534            None => match (brush.override_fill_color, brush.link_color) {
535                (Some(color), _) => {
536                    let Some(color_brush) = item_renderer.platform_brush_for_color(&color) else {
537                        return;
538                    };
539                    (color_brush.clone(), &None)
540                }
541                (None, Some(color)) => {
542                    let Some(link_brush) = item_renderer.platform_brush_for_color(&color) else {
543                        return;
544                    };
545                    (link_brush.clone(), &None)
546                }
547                (None, None) => (default_fill_brush.clone(), &brush.stroke),
548            },
549        };
550
551        match stroke_style {
552            Some(TextStrokeStyle::Outside) => {
553                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
554
555                if let Some(stroke_brush) = default_stroke_brush.clone() {
556                    item_renderer.draw_glyph_run(
557                        run.font(),
558                        PhysicalLength::new(run.font_size()),
559                        normalized_coords,
560                        &synthesis,
561                        stroke_brush,
562                        para_y,
563                        &mut glyphs.iter().cloned(),
564                    );
565                }
566
567                item_renderer.draw_glyph_run(
568                    run.font(),
569                    PhysicalLength::new(run.font_size()),
570                    normalized_coords,
571                    &synthesis,
572                    fill_brush.clone(),
573                    para_y,
574                    &mut glyphs.into_iter(),
575                );
576            }
577            Some(TextStrokeStyle::Center) => {
578                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
579
580                item_renderer.draw_glyph_run(
581                    run.font(),
582                    PhysicalLength::new(run.font_size()),
583                    normalized_coords,
584                    &synthesis,
585                    fill_brush.clone(),
586                    para_y,
587                    &mut glyphs.iter().cloned(),
588                );
589
590                if let Some(stroke_brush) = default_stroke_brush.clone() {
591                    item_renderer.draw_glyph_run(
592                        run.font(),
593                        PhysicalLength::new(run.font_size()),
594                        normalized_coords,
595                        &synthesis,
596                        stroke_brush,
597                        para_y,
598                        &mut glyphs.into_iter(),
599                    );
600                }
601            }
602            None => {
603                item_renderer.draw_glyph_run(
604                    run.font(),
605                    PhysicalLength::new(run.font_size()),
606                    normalized_coords,
607                    &synthesis,
608                    fill_brush.clone(),
609                    para_y,
610                    glyphs_it,
611                );
612            }
613        }
614
615        let metrics = run.metrics();
616
617        // A decoration spans the whole run. Where a selection boundary cuts through it, the
618        // renderer clip that cuts the glyphs cuts the rectangle too.
619        if glyph_run.style().underline.is_some() {
620            item_renderer.fill_rectangle(
621                PhysicalRect::new(
622                    PhysicalPoint::from_lengths(
623                        PhysicalLength::new(glyph_run.offset()),
624                        para_y
625                            + PhysicalLength::new(glyph_run.baseline() - metrics.underline_offset),
626                    ),
627                    PhysicalSize::new(glyph_run.advance(), metrics.underline_size),
628                ),
629                fill_brush.clone(),
630                PhysicalLength::zero(),
631                None,
632            );
633        }
634
635        if glyph_run.style().strikethrough.is_some() {
636            item_renderer.fill_rectangle(
637                PhysicalRect::new(
638                    PhysicalPoint::from_lengths(
639                        PhysicalLength::new(glyph_run.offset()),
640                        para_y
641                            + PhysicalLength::new(
642                                glyph_run.baseline() - metrics.strikethrough_offset,
643                            ),
644                    ),
645                    PhysicalSize::new(glyph_run.advance(), metrics.strikethrough_size),
646                ),
647                fill_brush,
648                PhysicalLength::zero(),
649                None,
650            );
651        }
652    }
653}
654
655impl Layout {
656    pub(super) fn draw<R: GlyphRenderer>(
657        &self,
658        item_renderer: &mut R,
659        default_fill_brush: <R as GlyphRenderer>::PlatformBrush,
660        default_stroke_brush: Option<<R as GlyphRenderer>::PlatformBrush>,
661        default_text_color: Color,
662        selection: Option<&SelectionRendering<'_, R>>,
663    ) {
664        // Compute the cut once: explicit `\n` breaks produce one paragraph each, but they must
665        // elide as a single block (drop lines below the box, ellipsis on the last visible one).
666        let visible_extent = self.visible_extent();
667
668        // Everything drawn below is cut to the renderer clip anyway, so lines that can't reach
669        // it are skipped instead of submitted: the clip is a bounding box of everything still
670        // drawable (see [`crate::item_rendering::ItemRenderer::get_current_clip`]), which makes
671        // skipping what lies outside it safe under any transform. The band only filters what is
672        // *drawn*; it never influences elision or `max-lines` accounting.
673        let visible_band = visible_band(item_renderer);
674        // The ellipsis is positioned from the overflowing run, so don't cull while eliding.
675        let visible_x_range = (!self.is_eliding()).then(|| visible_x_range(item_renderer));
676
677        // Paragraphs are stacked in order, so binary-search the first one whose box reaches the
678        // band. Start one paragraph earlier and stop one past the band: glyph ink may overhang
679        // its line's metrics box, and the line-level cull in [`TextParagraph::draw`] trims those
680        // two edge paragraphs down to their edge lines.
681        let first = self
682            .paragraphs
683            .partition_point(|p| {
684                self.y_offset + p.y + PhysicalLength::new(p.layout.height()) < visible_band.start
685            })
686            .saturating_sub(1);
687        let mut past_band = false;
688        for (paragraph_index, paragraph) in self.paragraphs.iter().enumerate().skip(first) {
689            if past_band {
690                break;
691            }
692            past_band = self.y_offset + paragraph.y > visible_band.end;
693            paragraph.draw(
694                self,
695                paragraph_index,
696                visible_extent,
697                &visible_band,
698                visible_x_range.as_ref(),
699                item_renderer,
700                &default_fill_brush,
701                &default_stroke_brush,
702                default_text_color,
703                selection,
704            );
705        }
706    }
707}