gallery/vendor/i-slint-core/textlayout/sharedparley/draw.rs
Greg Pomerantz 2f4768f354 Fix 120Hz scroll jank end-to-end: render-ahead present + full-cache eviction off the UI thread
Three root causes, all fixed and verified on the Pixel 9 Pro:

1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the
   render to the vsync wake; RENDER_AHEAD presents the previous frame at
   the start of render() (phase = wake + ~1.1 ms) and holds the new
   frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never
   merely dropped, before the next acquire (AlreadyAcquired panic at
   launch otherwise); discard_pending_frame presents too.

2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled
   through a ~20-cell viewport forever (137 decodes/20 s) and each
   eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes,
   ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone
   cooldown on evicted paths (idle prefetch skips, urgent tap bypasses)
   + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us).

3. (earlier) Skia final flush + Mailbox present mode (vendor, M3).

Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK
lines for cross-thread closure timing and push-site tagging.

Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact
120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero
>33 ms); PRES phase median ~994 us, zero late frames. Docs updated
(docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference).

Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch),
vkshim profiling tooling, backend made a non-optional dep, debuggable
flag for simpleperf (REMOVE before shipping).
2026-10-30 02:49:23 -04:00

708 lines
31 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
//! Painting a [`Layout`] through a [`GlyphRenderer`]: glyph runs, decorations, inline-code
//! capsules, and the clip-based recoloring of selected glyphs.
use super::layout::ElisionCut;
use super::selection::{RunCoverage, SelectionSpan, run_coverage};
use super::shaping::{Brush, TextParagraph};
use super::*;
/// Outline drawn around a rectangle filled via [`GlyphRenderer::fill_rectangle`].
#[derive(Clone)]
pub struct RectangleBorder<Brush> {
pub brush: Brush,
pub width: PhysicalLength,
}
/// Trait used for drawing text and text input elements with parley, where parley does the
/// shaping and positioning, and the renderer is responsible for drawing just the glyphs.
pub trait GlyphRenderer: crate::item_rendering::ItemRenderer {
/// A renderer-specific type for a brush used for fill and stroke of glyphs.
type PlatformBrush: Clone;
/// Returns the brush to be used for filling text.
fn platform_text_fill_brush(
&mut self,
brush: crate::Brush,
size: LogicalSize,
) -> Option<Self::PlatformBrush>;
/// Returns a brush that's a solid fill of the specified color.
fn platform_brush_for_color(&mut self, color: &Color) -> Option<Self::PlatformBrush>;
/// Returns the brush to be used for stroking text.
fn platform_text_stroke_brush(
&mut self,
brush: crate::Brush,
physical_stroke_width: f32,
size: LogicalSize,
) -> Option<Self::PlatformBrush>;
/// Draws the glyphs provided by glyphs_it with the specified font, font_size, and brush at the
/// given y offset. The `normalized_coords` are F2Dot14 values in fvar axis order for variable
/// font rendering. The `synthesis` contains design-space variation settings and faux
/// bold/italic hints from fontique.
fn draw_glyph_run(
&mut self,
font: &parley::FontData,
font_size: PhysicalLength,
normalized_coords: &[i16],
synthesis: &fontique::Synthesis,
brush: Self::PlatformBrush,
y_offset: PhysicalLength,
glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
);
/// Convenience wrapper around `fill_rectangle` that resolves `color` to a platform
/// brush and fills `physical_rect` with sharp corners and no outline.
fn fill_rectangle_with_color(&mut self, physical_rect: PhysicalRect, color: Color) {
if let Some(platform_brush) = self.platform_brush_for_color(&color) {
self.fill_rectangle(physical_rect, platform_brush, PhysicalLength::zero(), None);
}
}
/// Fills `physical_rect` with `brush`, optionally rounding the corners by `radius`
/// and outlining it with `border`. Passing a zero `radius` produces sharp corners;
/// passing `None` for `border` skips the outline.
fn fill_rectangle(
&mut self,
physical_rect: PhysicalRect,
brush: Self::PlatformBrush,
radius: PhysicalLength,
border: Option<RectangleBorder<Self::PlatformBrush>>,
);
}
/// The vertical extent the renderer clip lets anything be drawn in, as a physical y range in the
/// current item's coordinates. A conservative superset of what is visible -- see
/// [`crate::item_rendering::ItemRenderer::get_current_clip`] -- so it is a safe bound for
/// skipping draw work, never for layout decisions.
pub(super) fn visible_band(item_renderer: &impl GlyphRenderer) -> Range<PhysicalLength> {
let scale_factor = item_renderer.scale_factor();
let clip = item_renderer.get_current_clip();
let top = clip.origin.y_length() * scale_factor;
top..(top + clip.height_length() * scale_factor)
}
/// The horizontal counterpart of [`visible_band`].
pub(super) fn visible_x_range(item_renderer: &impl GlyphRenderer) -> Range<PhysicalLength> {
let scale_factor = item_renderer.scale_factor();
let x_range = item_renderer.get_current_clip().x_length_range();
(x_range.start * scale_factor)..(x_range.end * scale_factor)
}
impl TextParagraph {
#[allow(clippy::too_many_arguments)]
fn draw<R: GlyphRenderer>(
&self,
layout: &Layout,
paragraph_index: usize,
visible_extent: Option<ElisionCut>,
visible_band: &Range<PhysicalLength>,
// `None` when eliding.
visible_x_range: Option<&Range<PhysicalLength>>,
item_renderer: &mut R,
default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
default_text_color: Color,
selection: Option<&SelectionRendering<'_, R>>,
) {
let para_y = layout.y_offset + self.y;
let line_count = self.layout.lines().len();
// For `overflow: elide` with a height limit (`overflow: clip` applies a hard pixel clip
// instead) and for `max-lines`, `visible_extent` decides -- across all paragraphs -- the
// last line to keep and where the vertical-truncation ellipsis goes. Translate it to this
// paragraph. `last_drawn` is the deepest line of this paragraph that we draw; it carries
// the horizontal ellipsis when it overflows the width. `vertical_truncation` marks the
// single global last kept line that must also show an ellipsis when lines below it were
// dropped.
let (last_drawn, vertical_truncation) = match visible_extent {
// Entirely below the kept block: drop the paragraph (don't redraw a stray first line,
// and don't paint inline-code backgrounds under text that isn't rendered).
Some(cut) if paragraph_index > cut.last_paragraph => return,
// The paragraph where the cut falls: stop at the global last kept line.
Some(cut) if paragraph_index == cut.last_paragraph => {
(cut.last_line, cut.needs_ellipsis)
}
// A paragraph fully above the cut, or no cut at all: draw every line that fits
// the box; the last visual line still elides horizontally when it is too wide.
_ => (line_count.saturating_sub(1), false),
};
self.draw_inline_code_backgrounds(item_renderer, para_y, default_text_color, last_drawn);
for (index, line) in self.layout.lines().enumerate() {
// Stop once we are past the last kept line of the last kept paragraph.
if index > last_drawn {
break;
}
let metrics = line.metrics();
// Skip lines that can't reach the visible band. Ink may overhang the line's metrics
// box (stacked diacritics, swashes), so pad by one line height on each side before
// excluding -- the pad scales with the line itself. Lines are in block order, so the
// first line past the band ends the walk.
let line_height =
PhysicalLength::new(metrics.block_max_coord - metrics.block_min_coord);
if para_y + PhysicalLength::new(metrics.block_max_coord) + line_height
< visible_band.start
{
continue;
}
if para_y + PhysicalLength::new(metrics.block_min_coord) - line_height
> visible_band.end
{
break;
}
// The kept line is always drawn, even when it slightly exceeds the box (#12197); other
// lines are kept only while they fall within the box, taking vertical alignment into
// account (bottom/center alignment clips lines off the top, not the bottom).
let last_line = index == last_drawn;
if !last_line
&& !layout.paragraph_line_within_box(
self,
metrics.block_min_coord,
metrics.block_max_coord,
)
{
continue;
}
// The last drawn line should show an ellipsis if real lines below it were dropped for
// the height, even when it fits the width.
let vertically_truncated = last_line && vertical_truncation;
let line_spans =
selection.map(|selection| selection.spans.for_line(paragraph_index, index));
// Padded for ink overhanging the advance, like the vertical filtering of lines.
let padded_x_range = visible_x_range
.map(|x_range| (x_range.start - line_height)..(x_range.end + line_height));
for item in line.items() {
match item {
parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
let mut glyph_x_range = None;
if let Some(x_range) = &padded_x_range {
let run_start = PhysicalLength::new(glyph_run.offset());
let run_end =
PhysicalLength::new(glyph_run.offset() + glyph_run.advance());
if run_end < x_range.start || run_start > x_range.end {
continue;
}
if run_start < x_range.start || run_end > x_range.end {
glyph_x_range = Some(x_range);
}
}
let ellipsis = if last_line {
let (truncated_glyphs, ellipsis) = layout.glyphs_with_elision(
&glyph_run,
vertically_truncated,
metrics.trailing_whitespace,
);
Self::draw_glyph_run_with_selection(
&glyph_run,
item_renderer,
default_fill_brush,
default_stroke_brush,
para_y,
glyph_x_range,
&mut truncated_glyphs.into_iter(),
selection.map(|selection| &selection.foreground),
line_spans.unwrap_or_default(),
);
ellipsis
} else {
Self::draw_glyph_run_with_selection(
&glyph_run,
item_renderer,
default_fill_brush,
default_stroke_brush,
para_y,
glyph_x_range,
&mut glyph_run.positioned_glyphs(),
selection.map(|selection| &selection.foreground),
line_spans.unwrap_or_default(),
);
None
};
if let Some((ellipsis_glyph, ellipsis_font, font_size)) = ellipsis {
let run = glyph_run.run();
item_renderer.draw_glyph_run(
&ellipsis_font,
font_size,
run.normalized_coords(),
&run.synthesis(),
default_fill_brush.clone(),
para_y,
&mut core::iter::once(ellipsis_glyph),
);
}
}
parley::PositionedLayoutItem::InlineBox(_inline_box) => {}
};
}
}
}
/// Paints a translucent rounded capsule under every glyph run that lies inside one of
/// this paragraph's `Style::Code` ranges. Capsule colors are derived from the luminance
/// of `default_text_color`, so light and dark themes both get a sensible default
/// without any user-facing styling property.
fn draw_inline_code_backgrounds<R: GlyphRenderer>(
&self,
item_renderer: &mut R,
para_y: PhysicalLength,
default_text_color: Color,
last_drawn: usize,
) {
if self.code_ranges.is_empty() {
return;
}
// Neutral gray fill (low alpha) on both themes — contrast against the page
// background carries the "this is code" cue. The border picks up the same hue
// but a higher alpha so the rounded outline stays visible against the fill.
// Pick brighter values on dark backgrounds (luminance of the text gives us
// that signal without poking at the window background).
let fg_luminance = 0.299 * default_text_color.red() as f32
+ 0.587 * default_text_color.green() as f32
+ 0.114 * default_text_color.blue() as f32;
let fill = Color::from_argb_u8(28, 128, 128, 128);
let border = if fg_luminance > 140.0 {
Color::from_argb_u8(88, 170, 170, 170)
} else {
Color::from_argb_u8(56, 128, 128, 128)
};
// Border width and radius bounds are logical so that the capsule looks the
// same at every DPI; the part of the radius derived from the capsule height
// already scales with the (physical) font size.
const BORDER_WIDTH: LogicalLength = LogicalLength::new(1.0);
const MIN_RADIUS: LogicalLength = LogicalLength::new(2.0);
const MAX_RADIUS: LogicalLength = LogicalLength::new(5.0);
// A touch of vertical padding above and below the cap-height / descender band
// so the capsule edge doesn't sit flush against tall glyphs.
const VERTICAL_PADDING_RATIO: f32 = 0.15;
let scale_factor = item_renderer.scale_factor();
let border_width = BORDER_WIDTH * scale_factor;
// Capsules only under lines that are drawn: lines past the visible-extent cut
// (`overflow: elide` height limit or `max-lines`) don't render their glyphs either.
for line in self.layout.lines().take(last_drawn + 1) {
for item in line.items() {
let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else {
continue;
};
let run = glyph_run.run();
let run_range = run.text_range();
if run_range.is_empty() {
continue;
}
// `Style::Code` pushes its own FontFamily + FontSize, which forces a
// run boundary, so a code run is always fully contained in one of the
// recorded ranges — a single containment check is enough.
let is_code = self
.code_ranges
.iter()
.any(|cr| cr.start <= run_range.start && run_range.end <= cr.end);
if !is_code {
continue;
}
let metrics = run.metrics();
let ascent = metrics.ascent;
let descent = metrics.descent;
let cap_height = metrics.cap_height.unwrap_or(ascent * 0.72);
// Center the capsule on the midpoint between cap-top and a shallow
// approximation of the descender bottom (roughly where parens, commas
// and dots reach). This gives equal visible padding above and below
// for typical code text (which has caps but rarely real descenders).
let upper_extent = cap_height;
let lower_extent = descent * 0.4;
let center = glyph_run.baseline() + (lower_extent - upper_extent) / 2.0;
let inner_half_height = (upper_extent + lower_extent) / 2.0;
let extra_padding = ascent * VERTICAL_PADDING_RATIO;
let half_height = inner_half_height + extra_padding;
let bg_height = (half_height * 2.0).max(1.0);
let bg_top = center - half_height;
// Width hugs the glyphs tightly — `glyph_run.advance()` is exactly
// the horizontal extent of the rendered run. The underlying text is
// not modified, so selection, hit-testing and copy/paste keep working
// on the underlying characters.
let bg_width = glyph_run.advance().max(0.0);
if bg_width <= 0.0 {
continue;
}
let bg_left = glyph_run.offset();
let bg_rect = PhysicalRect::new(
PhysicalPoint::from_lengths(
PhysicalLength::new(bg_left),
PhysicalLength::new(bg_top) + para_y,
),
PhysicalSize::new(bg_width, bg_height),
);
let radius = PhysicalLength::new(bg_height * 0.22)
.max(MIN_RADIUS * scale_factor)
.min(MAX_RADIUS * scale_factor);
let Some(fill_brush) = item_renderer.platform_brush_for_color(&fill) else {
continue;
};
let border_brush = item_renderer
.platform_brush_for_color(&border)
.map(|brush| RectangleBorder { brush, width: border_width });
item_renderer.fill_rectangle(bg_rect, fill_brush, radius, border_brush);
}
}
}
/// Draws one glyph run, splitting it where the selection starts or ends inside it.
///
/// The overwhelmingly common cases -- a run that is entirely selected or entirely unselected
/// -- draw exactly once with no clip, so an enormous selection costs no more than a tiny one.
/// Only the at most two runs per selection edge that actually straddle a boundary are drawn
/// twice against a clip, and that is precisely where a ligature has to be cut in half.
#[allow(clippy::too_many_arguments)]
fn draw_glyph_run_with_selection<R: GlyphRenderer>(
glyph_run: &parley::layout::GlyphRun<Brush>,
item_renderer: &mut R,
default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
para_y: PhysicalLength,
// A uniform `no-wrap` line is a single run, so culling whole runs is not enough.
visible_x_range: Option<&Range<PhysicalLength>>,
glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
// The selection foreground, and the spans it covers on this run's line. Both empty when
// there is no selection, which `run_coverage` reports as `Unselected`.
selection_brush: Option<&<R as GlyphRenderer>::PlatformBrush>,
line_spans: &[SelectionSpan],
) {
let run_x = glyph_run.offset()..glyph_run.offset() + glyph_run.advance();
// Bidirectional text reorders glyphs within a run, so this filters rather than truncating.
let x_range = visible_x_range.cloned();
let mut glyphs_it = glyphs_it.filter(move |glyph| {
x_range.as_ref().is_none_or(|x_range| {
let start = PhysicalLength::new(glyph.x);
let end = PhysicalLength::new(glyph.x + glyph.advance);
end >= x_range.start && start <= x_range.end
})
});
let glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph> = &mut glyphs_it;
match run_coverage(&run_x, line_spans) {
RunCoverage::Unselected => Self::draw_glyph_run(
glyph_run,
item_renderer,
default_fill_brush,
default_stroke_brush,
para_y,
glyphs_it,
None,
),
RunCoverage::Full => Self::draw_glyph_run(
glyph_run,
item_renderer,
default_fill_brush,
default_stroke_brush,
para_y,
glyphs_it,
selection_brush,
),
RunCoverage::Partial => {
// The run has to be rasterized once per segment, so the glyphs can't stay behind
// a one-shot iterator.
let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
// Walk the run left to right, alternating unselected and selected segments. This
// relies on the spans being ascending in x, which [`SelectionSpans`] guarantees.
let mut x = run_x.start;
for span in line_spans {
let span_x = span.x();
if span_x.end <= run_x.start {
continue;
}
if span_x.start >= run_x.end {
break;
}
let start = span_x.start.max(run_x.start);
let end = span_x.end.min(run_x.end);
for (segment, brush) in [(x..start, None), (start..end, selection_brush)] {
Self::draw_glyph_run_segment(
glyph_run,
item_renderer,
default_fill_brush,
default_stroke_brush,
para_y,
&glyphs,
segment,
brush,
);
}
x = end;
}
Self::draw_glyph_run_segment(
glyph_run,
item_renderer,
default_fill_brush,
default_stroke_brush,
para_y,
&glyphs,
x..run_x.end,
None,
);
}
}
}
/// Draws `glyphs` clipped to the horizontal band `x`, so that a glyph straddling the band's
/// edge is cut rather than recolored as a whole.
fn draw_glyph_run_segment<R: GlyphRenderer>(
glyph_run: &parley::layout::GlyphRun<Brush>,
item_renderer: &mut R,
default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
para_y: PhysicalLength,
glyphs: &[parley::layout::Glyph],
x: Range<f32>,
override_fill_brush: Option<&<R as GlyphRenderer>::PlatformBrush>,
) {
if x.end <= x.start {
return;
}
item_renderer.save_state();
// Clip horizontally only: the vertical extent stays whatever is already in effect, so
// accents and descenders reaching outside the line box are never sheared off.
let scale_factor = item_renderer.scale_factor();
let current_clip = item_renderer.get_current_clip();
let render = item_renderer.combine_clip(
LogicalRect::new(
LogicalPoint::from_lengths(
PhysicalLength::new(x.start) / scale_factor,
current_clip.origin.y_length(),
),
LogicalSize::from_lengths(
PhysicalLength::new(x.end - x.start) / scale_factor,
current_clip.height_length(),
),
),
LogicalBorderRadius::zero(),
);
if render {
Self::draw_glyph_run(
glyph_run,
item_renderer,
default_fill_brush,
default_stroke_brush,
para_y,
&mut glyphs.iter().cloned(),
override_fill_brush,
);
}
item_renderer.restore_state();
}
fn draw_glyph_run<R: GlyphRenderer>(
glyph_run: &parley::layout::GlyphRun<Brush>,
item_renderer: &mut R,
default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
para_y: PhysicalLength,
glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
// Forced fill for selected glyphs, overriding the run's own brush.
override_fill_brush: Option<&<R as GlyphRenderer>::PlatformBrush>,
) {
let run = glyph_run.run();
let normalized_coords = run.normalized_coords();
let synthesis = run.synthesis();
let brush = &glyph_run.style().brush;
let (fill_brush, stroke_style) = match override_fill_brush {
// Selection wins over a `Style::Color` span and over a link color: text under the
// highlight has to stay legible against the selection background.
Some(selection_brush) => (selection_brush.clone(), &None),
None => match (brush.override_fill_color, brush.link_color) {
(Some(color), _) => {
let Some(color_brush) = item_renderer.platform_brush_for_color(&color) else {
return;
};
(color_brush.clone(), &None)
}
(None, Some(color)) => {
let Some(link_brush) = item_renderer.platform_brush_for_color(&color) else {
return;
};
(link_brush.clone(), &None)
}
(None, None) => (default_fill_brush.clone(), &brush.stroke),
},
};
match stroke_style {
Some(TextStrokeStyle::Outside) => {
let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
if let Some(stroke_brush) = default_stroke_brush.clone() {
item_renderer.draw_glyph_run(
run.font(),
PhysicalLength::new(run.font_size()),
normalized_coords,
&synthesis,
stroke_brush,
para_y,
&mut glyphs.iter().cloned(),
);
}
item_renderer.draw_glyph_run(
run.font(),
PhysicalLength::new(run.font_size()),
normalized_coords,
&synthesis,
fill_brush.clone(),
para_y,
&mut glyphs.into_iter(),
);
}
Some(TextStrokeStyle::Center) => {
let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
item_renderer.draw_glyph_run(
run.font(),
PhysicalLength::new(run.font_size()),
normalized_coords,
&synthesis,
fill_brush.clone(),
para_y,
&mut glyphs.iter().cloned(),
);
if let Some(stroke_brush) = default_stroke_brush.clone() {
item_renderer.draw_glyph_run(
run.font(),
PhysicalLength::new(run.font_size()),
normalized_coords,
&synthesis,
stroke_brush,
para_y,
&mut glyphs.into_iter(),
);
}
}
None => {
item_renderer.draw_glyph_run(
run.font(),
PhysicalLength::new(run.font_size()),
normalized_coords,
&synthesis,
fill_brush.clone(),
para_y,
glyphs_it,
);
}
}
let metrics = run.metrics();
// A decoration spans the whole run. Where a selection boundary cuts through it, the
// renderer clip that cuts the glyphs cuts the rectangle too.
if glyph_run.style().underline.is_some() {
item_renderer.fill_rectangle(
PhysicalRect::new(
PhysicalPoint::from_lengths(
PhysicalLength::new(glyph_run.offset()),
para_y
+ PhysicalLength::new(glyph_run.baseline() - metrics.underline_offset),
),
PhysicalSize::new(glyph_run.advance(), metrics.underline_size),
),
fill_brush.clone(),
PhysicalLength::zero(),
None,
);
}
if glyph_run.style().strikethrough.is_some() {
item_renderer.fill_rectangle(
PhysicalRect::new(
PhysicalPoint::from_lengths(
PhysicalLength::new(glyph_run.offset()),
para_y
+ PhysicalLength::new(
glyph_run.baseline() - metrics.strikethrough_offset,
),
),
PhysicalSize::new(glyph_run.advance(), metrics.strikethrough_size),
),
fill_brush,
PhysicalLength::zero(),
None,
);
}
}
}
impl Layout {
pub(super) fn draw<R: GlyphRenderer>(
&self,
item_renderer: &mut R,
default_fill_brush: <R as GlyphRenderer>::PlatformBrush,
default_stroke_brush: Option<<R as GlyphRenderer>::PlatformBrush>,
default_text_color: Color,
selection: Option<&SelectionRendering<'_, R>>,
) {
// Compute the cut once: explicit `\n` breaks produce one paragraph each, but they must
// elide as a single block (drop lines below the box, ellipsis on the last visible one).
let visible_extent = self.visible_extent();
// Everything drawn below is cut to the renderer clip anyway, so lines that can't reach
// it are skipped instead of submitted: the clip is a bounding box of everything still
// drawable (see [`crate::item_rendering::ItemRenderer::get_current_clip`]), which makes
// skipping what lies outside it safe under any transform. The band only filters what is
// *drawn*; it never influences elision or `max-lines` accounting.
let visible_band = visible_band(item_renderer);
// The ellipsis is positioned from the overflowing run, so don't cull while eliding.
let visible_x_range = (!self.is_eliding()).then(|| visible_x_range(item_renderer));
// Paragraphs are stacked in order, so binary-search the first one whose box reaches the
// band. Start one paragraph earlier and stop one past the band: glyph ink may overhang
// its line's metrics box, and the line-level cull in [`TextParagraph::draw`] trims those
// two edge paragraphs down to their edge lines.
let first = self
.paragraphs
.partition_point(|p| {
self.y_offset + p.y + PhysicalLength::new(p.layout.height()) < visible_band.start
})
.saturating_sub(1);
let mut past_band = false;
for (paragraph_index, paragraph) in self.paragraphs.iter().enumerate().skip(first) {
if past_band {
break;
}
past_band = self.y_offset + paragraph.y > visible_band.end;
paragraph.draw(
self,
paragraph_index,
visible_extent,
&visible_band,
visible_x_range.as_ref(),
item_renderer,
&default_fill_brush,
&default_stroke_brush,
default_text_color,
selection,
);
}
}
}