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}