Skip to main content

i_slint_core/textlayout/
sharedparley.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// cSpell: ignore RAII
5pub use parley;
6pub use parley::fontique;
7
8use crate::item_rendering::HasFont;
9use crate::{
10    Color,
11    graphics::FontRequest,
12    item_rendering::PlainOrStyledText,
13    items::TextStrokeStyle,
14    lengths::{
15        LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx,
16        PointLengths, ScaleFactor, SizeLengths,
17    },
18    renderer::RendererSealed,
19    textlayout::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap},
20};
21use alloc::vec::Vec;
22use core::ops::Range;
23use core::pin::Pin;
24use euclid::num::Zero;
25use i_slint_common::sharedfontique;
26use skrifa::MetadataProvider as _;
27use std::cell::RefCell;
28use std::collections::HashSet;
29use std::sync::Arc;
30
31#[derive(derive_more::Deref, derive_more::DerefMut)]
32pub struct FontContext {
33    #[deref]
34    #[deref_mut]
35    pub inner: parley::FontContext,
36    /// `(ptr, len)` of each `&'static [u8]` already handed to fontique, so repeat
37    /// `register_static_font` calls for the same embedded font are skipped.
38    registered_static_fonts: HashSet<(usize, usize)>,
39}
40
41impl FontContext {
42    pub fn new(inner: parley::FontContext) -> Self {
43        Self { inner, registered_static_fonts: HashSet::default() }
44    }
45
46    pub fn register_static_font(&mut self, data: &'static [u8]) {
47        let key = (data.as_ptr() as usize, data.len());
48        if self.registered_static_fonts.insert(key) {
49            self.inner.collection.register_fonts(fontique::Blob::new(Arc::new(data)), None);
50        }
51    }
52
53    pub fn clear_registered_static_fonts(&mut self) {
54        self.registered_static_fonts.clear();
55    }
56}
57
58type InnerTextLayoutCache = crate::item_rendering::ItemCache<Vec<TextParagraph>>;
59
60/// Cache for shaped text paragraphs (before line breaking), keyed by ItemRc.
61pub struct TextLayoutCache {
62    inner: InnerTextLayoutCache,
63    #[cfg(feature = "testing")]
64    cache_miss_count: std::cell::Cell<u64>,
65}
66
67#[allow(clippy::derivable_impls)] // clippy doesn't see the feature = "testing" code
68impl Default for TextLayoutCache {
69    fn default() -> Self {
70        Self {
71            inner: Default::default(),
72            #[cfg(feature = "testing")]
73            cache_miss_count: std::cell::Cell::new(0),
74        }
75    }
76}
77
78impl TextLayoutCache {
79    pub fn clear_cache_if_scale_factor_changed(&self, window: &crate::api::Window) {
80        self.inner.clear_cache_if_scale_factor_changed(window);
81    }
82    pub fn component_destroyed(&self, component: crate::item_tree::ItemTreeRef) {
83        self.inner.component_destroyed(component);
84    }
85    pub fn clear_all(&self) {
86        self.inner.clear_all();
87    }
88}
89
90#[cfg(feature = "testing")]
91impl TextLayoutCache {
92    pub fn cache_miss_count(&self) -> u64 {
93        self.cache_miss_count.get()
94    }
95    pub fn reset_cache_miss_count(&self) {
96        self.cache_miss_count.set(0);
97    }
98}
99
100pub type PhysicalLength = euclid::Length<f32, PhysicalPx>;
101pub type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
102type PhysicalSize = euclid::Size2D<f32, PhysicalPx>;
103type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>;
104
105/// Trait used for drawing text and text input elements with parley, where parley does the
106/// shaping and positioning, and the renderer is responsible for drawing just the glyphs.
107pub trait GlyphRenderer: crate::item_rendering::ItemRenderer {
108    /// A renderer-specific type for a brush used for fill and stroke of glyphs.
109    type PlatformBrush: Clone;
110
111    /// Returns the brush to be used for filling text.
112    fn platform_text_fill_brush(
113        &mut self,
114        brush: crate::Brush,
115        size: LogicalSize,
116    ) -> Option<Self::PlatformBrush>;
117
118    /// Returns a brush that's a solid fill of the specified color.
119    fn platform_brush_for_color(&mut self, color: &Color) -> Option<Self::PlatformBrush>;
120
121    /// Returns the brush to be used for stroking text.
122    fn platform_text_stroke_brush(
123        &mut self,
124        brush: crate::Brush,
125        physical_stroke_width: f32,
126        size: LogicalSize,
127    ) -> Option<Self::PlatformBrush>;
128
129    /// Draws the glyphs provided by glyphs_it with the specified font, font_size, and brush at the
130    /// given y offset. The `normalized_coords` are F2Dot14 values in fvar axis order for variable
131    /// font rendering. The `synthesis` contains design-space variation settings and faux
132    /// bold/italic hints from fontique.
133    fn draw_glyph_run(
134        &mut self,
135        font: &parley::FontData,
136        font_size: PhysicalLength,
137        normalized_coords: &[i16],
138        synthesis: &fontique::Synthesis,
139        brush: Self::PlatformBrush,
140        y_offset: PhysicalLength,
141        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
142    );
143
144    fn fill_rectangle_with_color(&mut self, physical_rect: PhysicalRect, color: Color) {
145        if let Some(platform_brush) = self.platform_brush_for_color(&color) {
146            self.fill_rectangle(physical_rect, platform_brush);
147        }
148    }
149
150    /// Fills the given rectangle with the specified color. This is used for drawing selection
151    /// rectangles as well as the text cursor.
152    fn fill_rectangle(&mut self, physical_rect: PhysicalRect, brush: Self::PlatformBrush);
153}
154
155pub use super::DEFAULT_FONT_SIZE;
156
157std::thread_local! {
158    static LAYOUT_CONTEXT: RefCell<parley::LayoutContext<Brush>> = Default::default();
159}
160
161#[derive(Debug, Default, PartialEq, Clone, Copy)]
162struct Brush {
163    /// When set, this overrides the fill/stroke to use this color.
164    override_fill_color: Option<Color>,
165    stroke: Option<TextStrokeStyle>,
166    link_color: Option<Color>,
167}
168
169#[derive(Default)]
170struct LayoutOptions {
171    max_width: Option<LogicalLength>,
172    max_height: Option<LogicalLength>,
173    horizontal_align: TextHorizontalAlignment,
174    vertical_align: TextVerticalAlignment,
175    text_overflow: TextOverflow,
176}
177
178impl LayoutOptions {
179    fn new_from_textinput(
180        text_input: Pin<&crate::items::TextInput>,
181        max_width: Option<LogicalLength>,
182        max_height: Option<LogicalLength>,
183    ) -> Self {
184        Self {
185            max_width,
186            max_height,
187            horizontal_align: text_input.horizontal_alignment(),
188            vertical_align: text_input.vertical_alignment(),
189            text_overflow: TextOverflow::Clip,
190        }
191    }
192}
193
194struct LayoutWithoutLineBreaksBuilder {
195    font_request: Option<FontRequest>,
196    text_wrap: TextWrap,
197    stroke: Option<TextStrokeStyle>,
198    scale_factor: ScaleFactor,
199    pixel_size: LogicalLength,
200}
201
202impl LayoutWithoutLineBreaksBuilder {
203    fn new(
204        font_request: Option<FontRequest>,
205        text_wrap: TextWrap,
206        stroke: Option<TextStrokeStyle>,
207        scale_factor: ScaleFactor,
208    ) -> Self {
209        let pixel_size = font_request
210            .as_ref()
211            .and_then(|font_request| font_request.pixel_size)
212            .unwrap_or(DEFAULT_FONT_SIZE);
213
214        Self { font_request, text_wrap, stroke, scale_factor, pixel_size }
215    }
216
217    fn ranged_builder<'a>(
218        &self,
219        layout_ctx: &'a mut parley::LayoutContext<Brush>,
220        font_ctx: &'a mut parley::FontContext,
221        text: &'a str,
222    ) -> parley::RangedBuilder<'a, Brush> {
223        // Pin the line height to the requested font's metrics so fallback runs (e.g.
224        // password chars rendered by a wider-descender symbol font) don't grow the
225        // line box. FontSizeRelative makes the ratio scale with any per-span FontSize.
226        let line_height_ratio = self.font_request.as_ref().and_then(|font_request| {
227            let font = font_request
228                .clone()
229                .query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
230            let face = skrifa::FontRef::from_index(font.blob.data(), font.index).ok()?;
231            let location = face.axes().location(font.synthesis.variation_settings());
232            let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
233            let units_per_em = metrics.units_per_em as f32;
234            (units_per_em > 0.0)
235                .then(|| (metrics.ascent - metrics.descent + metrics.leading) / units_per_em)
236        });
237
238        let mut builder = layout_ctx.ranged_builder(font_ctx, text, self.scale_factor.get(), false);
239
240        if let Some(ratio) = line_height_ratio {
241            builder.push_default(parley::StyleProperty::LineHeight(
242                parley::style::LineHeight::FontSizeRelative(ratio),
243            ));
244        }
245
246        if let Some(ref font_request) = self.font_request {
247            let mut fallback_family_iter = sharedfontique::FALLBACK_FAMILIES
248                .into_iter()
249                .map(parley::style::FontFamilyName::Generic);
250
251            let font_families: &[parley::style::FontFamilyName] = if let Some(family) =
252                &font_request.family
253            {
254                let mut iter =
255                    core::iter::once(parley::style::FontFamilyName::named(family.as_str()))
256                        .chain(fallback_family_iter);
257                &core::array::from_fn::<
258                    _,
259                    { sharedfontique::FALLBACK_FAMILIES.as_slice().len() + 1 },
260                    _,
261                >(|_| iter.next().unwrap())
262            } else {
263                &core::array::from_fn::<_, { sharedfontique::FALLBACK_FAMILIES.as_slice().len() }, _>(
264                    |_| fallback_family_iter.next().unwrap(),
265                )
266            };
267
268            builder.push_default(parley::style::FontFamily::List(std::borrow::Cow::Borrowed(
269                font_families,
270            )));
271
272            if let Some(weight) = font_request.weight {
273                builder.push_default(parley::StyleProperty::FontWeight(
274                    parley::style::FontWeight::new(weight as f32),
275                ));
276            }
277            if let Some(letter_spacing) = font_request.letter_spacing {
278                builder.push_default(parley::StyleProperty::LetterSpacing(letter_spacing.get()));
279            }
280            builder.push_default(parley::StyleProperty::FontStyle(if font_request.italic {
281                parley::style::FontStyle::Italic
282            } else {
283                parley::style::FontStyle::Normal
284            }));
285        }
286        builder.push_default(parley::StyleProperty::FontSize(self.pixel_size.get()));
287        builder.push_default(parley::StyleProperty::WordBreak(match self.text_wrap {
288            TextWrap::NoWrap => parley::style::WordBreak::KeepAll,
289            TextWrap::WordWrap => parley::style::WordBreak::Normal,
290            TextWrap::CharWrap => parley::style::WordBreak::BreakAll,
291        }));
292        builder.push_default(parley::StyleProperty::OverflowWrap(match self.text_wrap {
293            TextWrap::NoWrap => parley::style::OverflowWrap::Normal,
294            TextWrap::WordWrap | TextWrap::CharWrap => parley::style::OverflowWrap::Anywhere,
295        }));
296        if self.text_wrap == TextWrap::NoWrap {
297            // Parley 0.9 removed the width parameter from `Layout::align()` and instead
298            // uses the `max_advance` set by `break_all_lines()` as the alignment container
299            // width. To allow passing `max_physical_width` to `break_all_lines` for alignment
300            // purposes without triggering actual line wrapping, we must set `TextWrapMode::NoWrap`.
301            builder.push_default(parley::StyleProperty::TextWrapMode(
302                parley::style::TextWrapMode::NoWrap,
303            ));
304        }
305
306        builder.push_default(parley::StyleProperty::Brush(Brush {
307            override_fill_color: None,
308            stroke: self.stroke,
309            link_color: None,
310        }));
311
312        builder
313    }
314
315    fn build(
316        &self,
317        font_context: &mut parley::FontContext,
318        text: &str,
319        selection: Option<(Range<usize>, Color)>,
320        formatting: impl IntoIterator<Item = i_slint_common::styled_text::FormattedSpan>,
321        link_color: Option<Color>,
322    ) -> parley::Layout<Brush> {
323        use i_slint_common::styled_text::Style;
324
325        LAYOUT_CONTEXT.with_borrow_mut(|layout_ctx| {
326            let mut builder = self.ranged_builder(layout_ctx, font_context, text);
327
328            if let Some((selection_range, selection_color)) = selection {
329                {
330                    builder.push(
331                        parley::StyleProperty::Brush(Brush {
332                            override_fill_color: Some(selection_color),
333                            stroke: self.stroke,
334                            link_color: None,
335                        }),
336                        selection_range,
337                    );
338                }
339            }
340
341            // filter empty ranges otherwise parley will panic on assert
342            for span in formatting.into_iter().filter(|s| !s.range.is_empty()) {
343                match span.style {
344                    Style::Emphasis => {
345                        builder.push(
346                            parley::StyleProperty::FontStyle(parley::style::FontStyle::Italic),
347                            span.range,
348                        );
349                    }
350                    Style::Strikethrough => {
351                        builder.push(parley::StyleProperty::Strikethrough(true), span.range);
352                    }
353                    Style::Strong => {
354                        builder.push(
355                            parley::StyleProperty::FontWeight(parley::style::FontWeight::BOLD),
356                            span.range,
357                        );
358                    }
359                    Style::Code => {
360                        builder.push(
361                            parley::StyleProperty::FontFamily(parley::style::FontFamily::Single(
362                                parley::style::FontFamilyName::Generic(
363                                    parley::style::GenericFamily::Monospace,
364                                ),
365                            )),
366                            span.range,
367                        );
368                    }
369                    Style::Underline => {
370                        builder.push(parley::StyleProperty::Underline(true), span.range);
371                    }
372                    Style::Link => {
373                        builder.push(parley::StyleProperty::Underline(true), span.range.clone());
374                        builder.push(
375                            parley::StyleProperty::Brush(Brush {
376                                override_fill_color: None,
377                                stroke: self.stroke,
378                                link_color,
379                            }),
380                            span.range,
381                        );
382                    }
383                    Style::Color(color) => {
384                        builder.push(
385                            parley::StyleProperty::Brush(Brush {
386                                override_fill_color: Some(crate::Color::from_argb_encoded(color)),
387                                stroke: self.stroke,
388                                link_color: None,
389                            }),
390                            span.range,
391                        );
392                    }
393                }
394            }
395
396            builder.build(text)
397        })
398    }
399}
400
401/// Splits plain text into paragraph byte ranges at `'\n'`. The `'\n'` and any preceding `'\r'`
402/// are excluded from the range: parley treats a lone CR as a mandatory line break, so a CRLF
403/// left in the paragraph would render an extra empty line.
404fn paragraph_ranges(text: &str) -> impl Iterator<Item = Range<usize>> + '_ {
405    let mut start = 0;
406    text.split('\n').map(move |paragraph| {
407        let end = start + paragraph.len();
408        let range = if paragraph.ends_with('\r') { start..end - 1 } else { start..end };
409        start = end + 1;
410        range
411    })
412}
413
414fn create_text_paragraphs(
415    layout_builder: &LayoutWithoutLineBreaksBuilder,
416    font_context: &mut parley::FontContext,
417    text: PlainOrStyledText,
418    selection: Option<(Range<usize>, Color)>,
419    link_color: Color,
420) -> Vec<TextParagraph> {
421    let paragraph_from_text =
422        |font_context: &mut parley::FontContext,
423         text: &str,
424         range: std::ops::Range<usize>,
425         formatting: Vec<i_slint_common::styled_text::FormattedSpan>,
426         links: Vec<(std::ops::Range<usize>, std::string::String)>| {
427            let selection = selection.clone().and_then(|(selection, selection_color)| {
428                let sel_start = selection.start.max(range.start);
429                let sel_end = selection.end.min(range.end);
430
431                if sel_start < sel_end {
432                    let local_selection = (sel_start - range.start)..(sel_end - range.start);
433                    Some((local_selection, selection_color))
434                } else {
435                    None
436                }
437            });
438
439            let layout =
440                layout_builder.build(font_context, text, selection, formatting, Some(link_color));
441
442            TextParagraph { range, y: PhysicalLength::default(), layout, links }
443        };
444
445    let mut paragraphs = Vec::with_capacity(1);
446
447    match text {
448        PlainOrStyledText::Plain(ref text) => {
449            for range in paragraph_ranges(text) {
450                paragraphs.push(paragraph_from_text(
451                    font_context,
452                    &text[range.clone()],
453                    range,
454                    Default::default(),
455                    Default::default(),
456                ));
457            }
458        }
459        PlainOrStyledText::Styled(rich_text) => {
460            for paragraph in rich_text.paragraphs {
461                paragraphs.push(paragraph_from_text(
462                    font_context,
463                    &paragraph.text,
464                    0..0,
465                    paragraph.formatting,
466                    paragraph.links,
467                ));
468            }
469        }
470    };
471
472    paragraphs
473}
474
475/// Note: parley currently uses `WordBreak` while shaping via `analyze_text()`,
476/// so shaped paragraphs aren't identical across wrap modes. This is why `text_size()`
477/// doesn't use the `TextLayoutCache` — it would be incorrect to share cached paragraphs
478/// shaped with one wrap mode and reuse them with another.
479fn layout(
480    layout_builder: &LayoutWithoutLineBreaksBuilder,
481    font_context: &mut parley::FontContext,
482    mut paragraphs: Vec<TextParagraph>,
483    scale_factor: ScaleFactor,
484    options: LayoutOptions,
485) -> Layout {
486    let max_physical_width = options.max_width.map(|max_width| max_width * scale_factor);
487    let max_physical_height = options.max_height.map(|max_height| max_height * scale_factor);
488
489    // Returned None if failed to get the ellipsis glyph for some rare reason.
490    let get_ellipsis_glyph = |font_context: &mut parley::FontContext| {
491        let mut layout = layout_builder.build(font_context, "…", None, None, None);
492        layout.break_all_lines(None);
493        let line = layout.lines().next()?;
494        let item = line.items().next()?;
495        let run = match item {
496            parley::layout::PositionedLayoutItem::GlyphRun(run) => Some(run),
497            _ => return None,
498        }?;
499        let glyph = run.positioned_glyphs().next()?;
500        Some((glyph, run.run().font().clone()))
501    };
502
503    let elision_info = if let (TextOverflow::Elide, Some(max_physical_width)) =
504        (options.text_overflow, max_physical_width)
505    {
506        get_ellipsis_glyph(font_context).map(|(ellipsis_glyph, font_for_ellipsis_glyph)| {
507            ElisionInfo { ellipsis_glyph, font_for_ellipsis_glyph, max_physical_width }
508        })
509    } else {
510        None
511    };
512
513    let mut para_y = 0.0;
514    for para in paragraphs.iter_mut() {
515        para.layout.break_all_lines(max_physical_width.map(|width| width.get()));
516        para.layout.align(
517            match options.horizontal_align {
518                TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
519                    parley::Alignment::Left
520                }
521                TextHorizontalAlignment::Center => parley::Alignment::Center,
522                TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
523                    parley::Alignment::Right
524                }
525            },
526            parley::AlignmentOptions::default(),
527        );
528
529        para.y = PhysicalLength::new(para_y);
530        para_y += para.layout.height();
531    }
532
533    let max_width = paragraphs
534        .iter()
535        .map(|p| {
536            // The max width is used for the ellipsis computation when eliding text. We *want* to exclude whitespace
537            // for that, but we can't at the glyph run level, so the glyph runs always *do* include whitespace glyphs,
538            // and as such we must also accept the full width here including trailing whitespace, otherwise text with
539            // trailing whitespace will assigned a smaller width for rendering and thus the ellipsis will be placed.
540            PhysicalLength::new(p.layout.full_width())
541        })
542        .fold(PhysicalLength::zero(), PhysicalLength::max);
543    let height = paragraphs
544        .last()
545        .map_or(PhysicalLength::zero(), |p| p.y + PhysicalLength::new(p.layout.height()));
546
547    let y_offset = match (max_physical_height, options.vertical_align) {
548        (Some(max_height), TextVerticalAlignment::Center) => (max_height - height) / 2.0,
549        (Some(max_height), TextVerticalAlignment::Bottom) => max_height - height,
550        (None, _) | (Some(_), TextVerticalAlignment::Top) => PhysicalLength::new(0.0),
551    };
552
553    Layout { paragraphs, y_offset, elision_info, max_width, height, max_physical_height }
554}
555
556/// RAII guard: takes Vec out of the cache on creation, puts it back on drop.
557struct CachedParagraphsGuard<'a> {
558    paragraphs: Option<Vec<TextParagraph>>,
559    container: Option<std::cell::RefMut<'a, Vec<TextParagraph>>>,
560}
561
562impl Drop for CachedParagraphsGuard<'_> {
563    fn drop(&mut self) {
564        if let (Some(paragraphs), Some(container)) = (self.paragraphs.take(), &mut self.container) {
565            **container = paragraphs;
566        }
567    }
568}
569
570fn shape_paragraphs(
571    text: Pin<&dyn crate::item_rendering::RenderText>,
572    item_rc: Option<&crate::item_tree::ItemRc>,
573    scale_factor: ScaleFactor,
574    font_context: &mut parley::FontContext,
575) -> Vec<TextParagraph> {
576    let (stroke_brush, _, stroke_style) = text.stroke();
577    let has_stroke = !stroke_brush.is_transparent();
578    let builder = LayoutWithoutLineBreaksBuilder::new(
579        item_rc.map(|irc| text.font_request(irc)),
580        text.wrap(),
581        has_stroke.then_some(stroke_style),
582        scale_factor,
583    );
584    create_text_paragraphs(&builder, font_context, text.text(), None, text.link_color())
585}
586
587fn get_or_create_text_paragraphs<'a>(
588    cache: Option<&'a TextLayoutCache>,
589    item_rc: Option<&crate::item_tree::ItemRc>,
590    text: Pin<&dyn crate::item_rendering::RenderText>,
591    scale_factor: ScaleFactor,
592    font_context: &mut parley::FontContext,
593) -> CachedParagraphsGuard<'a> {
594    if let (Some(cache), Some(item_rc)) = (cache, item_rc) {
595        let mut entry = cache.inner.get_or_update_cache_entry_ref(item_rc, || {
596            #[cfg(feature = "testing")]
597            cache.cache_miss_count.set(cache.cache_miss_count.get() + 1);
598            shape_paragraphs(text, Some(item_rc), scale_factor, font_context)
599        });
600        let paragraphs = std::mem::take(&mut *entry);
601        CachedParagraphsGuard { paragraphs: Some(paragraphs), container: Some(entry) }
602    } else {
603        CachedParagraphsGuard {
604            paragraphs: Some(shape_paragraphs(text, item_rc, scale_factor, font_context)),
605            container: None,
606        }
607    }
608}
609
610struct ElisionInfo {
611    ellipsis_glyph: parley::layout::Glyph,
612    font_for_ellipsis_glyph: parley::FontData,
613    max_physical_width: PhysicalLength,
614}
615
616/// Whether a line whose bottom edge is at `block_max_coord` fits within `max_physical_height`,
617/// rounding the height up so a sub-pixel overflow still counts as fitting.
618fn line_fits_height(block_max_coord: f32, max_physical_height: PhysicalLength) -> bool {
619    max_physical_height.get().ceil() >= block_max_coord
620}
621
622struct TextParagraph {
623    range: Range<usize>,
624    y: PhysicalLength,
625    layout: parley::Layout<Brush>,
626    links: std::vec::Vec<(Range<usize>, std::string::String)>,
627}
628
629impl TextParagraph {
630    fn draw<R: GlyphRenderer>(
631        &self,
632        layout: &Layout,
633        paragraph_index: usize,
634        elision_extent: Option<ElisionCut>,
635        item_renderer: &mut R,
636        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
637        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
638        draw_glyphs: &mut dyn FnMut(
639            &mut R,
640            &parley::FontData,
641            PhysicalLength,
642            &[i16],               // normalized variation coords
643            &fontique::Synthesis, // design-space variation settings
644            <R as GlyphRenderer>::PlatformBrush,
645            PhysicalLength, // y offset for paragraph
646            &mut dyn Iterator<Item = parley::layout::Glyph>,
647        ),
648    ) {
649        let para_y = layout.y_offset + self.y;
650
651        let line_count = self.layout.lines().len();
652
653        // For `overflow: elide` with a height limit (`overflow: clip` applies a hard pixel clip
654        // instead), `elision_extent` decides -- across all paragraphs -- the last line to keep and
655        // where the vertical-truncation ellipsis goes. Translate it to this paragraph. `last_drawn`
656        // is the deepest line of this paragraph that we draw; it carries the horizontal ellipsis
657        // when it overflows the width. `vertical_truncation` marks the single global last kept line
658        // that must also show an ellipsis when lines below it were dropped for the height.
659        let (last_drawn, vertical_truncation) = match elision_extent {
660            // Entirely below the kept block: drop the paragraph (don't redraw a stray first line).
661            Some(cut) if paragraph_index > cut.last_paragraph => return,
662            // The paragraph where the height cut falls: stop at the global last kept line.
663            Some(cut) if paragraph_index == cut.last_paragraph => {
664                (cut.last_line, cut.needs_ellipsis)
665            }
666            // A paragraph fully above the cut, or no height limit at all: draw every line that fits
667            // the box; the last visual line still elides horizontally when it is too wide.
668            _ => (line_count.saturating_sub(1), false),
669        };
670
671        for (index, line) in self.layout.lines().enumerate() {
672            // Stop once we are past the last kept line of the last kept paragraph.
673            if index > last_drawn {
674                break;
675            }
676            let metrics = line.metrics();
677            // The kept line is always drawn, even when it slightly exceeds the box (#12197); other
678            // lines are kept only while they fall within the box, taking vertical alignment into
679            // account (bottom/center alignment clips lines off the top, not the bottom).
680            let last_line = index == last_drawn;
681            if !last_line
682                && !layout.paragraph_line_within_box(
683                    self,
684                    metrics.block_min_coord,
685                    metrics.block_max_coord,
686                )
687            {
688                continue;
689            }
690            // The last drawn line should show an ellipsis if real lines below it were dropped for
691            // the height, even when it fits the width.
692            let vertically_truncated = last_line && vertical_truncation;
693            for item in line.items() {
694                match item {
695                    parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
696                        let ellipsis = if last_line {
697                            let (truncated_glyphs, ellipsis) = layout.glyphs_with_elision(
698                                &glyph_run,
699                                vertically_truncated,
700                                metrics.trailing_whitespace,
701                            );
702
703                            Self::draw_glyph_run(
704                                &glyph_run,
705                                item_renderer,
706                                default_fill_brush,
707                                default_stroke_brush,
708                                para_y,
709                                &mut truncated_glyphs.into_iter(),
710                                draw_glyphs,
711                            );
712                            ellipsis
713                        } else {
714                            Self::draw_glyph_run(
715                                &glyph_run,
716                                item_renderer,
717                                default_fill_brush,
718                                default_stroke_brush,
719                                para_y,
720                                &mut glyph_run.positioned_glyphs(),
721                                draw_glyphs,
722                            );
723                            None
724                        };
725
726                        if let Some((ellipsis_glyph, ellipsis_font, font_size)) = ellipsis {
727                            let run = glyph_run.run();
728                            draw_glyphs(
729                                item_renderer,
730                                &ellipsis_font,
731                                font_size,
732                                run.normalized_coords(),
733                                &run.synthesis(),
734                                default_fill_brush.clone(),
735                                para_y,
736                                &mut core::iter::once(ellipsis_glyph),
737                            );
738                        }
739                    }
740                    parley::PositionedLayoutItem::InlineBox(_inline_box) => {}
741                };
742            }
743        }
744    }
745
746    fn draw_glyph_run<R: GlyphRenderer>(
747        glyph_run: &parley::layout::GlyphRun<Brush>,
748        item_renderer: &mut R,
749        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
750        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
751        para_y: PhysicalLength,
752        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
753        draw_glyphs: &mut dyn FnMut(
754            &mut R,
755            &parley::FontData,
756            PhysicalLength,
757            &[i16],               // normalized variation coords
758            &fontique::Synthesis, // design-space variation settings
759            <R as GlyphRenderer>::PlatformBrush,
760            PhysicalLength,
761            &mut dyn Iterator<Item = parley::layout::Glyph>,
762        ),
763    ) {
764        let run = glyph_run.run();
765        let normalized_coords = run.normalized_coords();
766        let synthesis = run.synthesis();
767        let brush = &glyph_run.style().brush;
768
769        let (fill_brush, stroke_style) = match (brush.override_fill_color, brush.link_color) {
770            (Some(color), _) => {
771                let Some(selection_brush) = item_renderer.platform_brush_for_color(&color) else {
772                    return;
773                };
774                (selection_brush.clone(), &None)
775            }
776            (None, Some(color)) => {
777                let Some(link_brush) = item_renderer.platform_brush_for_color(&color) else {
778                    return;
779                };
780                (link_brush.clone(), &None)
781            }
782            (None, None) => (default_fill_brush.clone(), &brush.stroke),
783        };
784
785        match stroke_style {
786            Some(TextStrokeStyle::Outside) => {
787                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
788
789                if let Some(stroke_brush) = default_stroke_brush.clone() {
790                    draw_glyphs(
791                        item_renderer,
792                        run.font(),
793                        PhysicalLength::new(run.font_size()),
794                        normalized_coords,
795                        &synthesis,
796                        stroke_brush,
797                        para_y,
798                        &mut glyphs.iter().cloned(),
799                    );
800                }
801
802                draw_glyphs(
803                    item_renderer,
804                    run.font(),
805                    PhysicalLength::new(run.font_size()),
806                    normalized_coords,
807                    &synthesis,
808                    fill_brush.clone(),
809                    para_y,
810                    &mut glyphs.into_iter(),
811                );
812            }
813            Some(TextStrokeStyle::Center) => {
814                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
815
816                draw_glyphs(
817                    item_renderer,
818                    run.font(),
819                    PhysicalLength::new(run.font_size()),
820                    normalized_coords,
821                    &synthesis,
822                    fill_brush.clone(),
823                    para_y,
824                    &mut glyphs.iter().cloned(),
825                );
826
827                if let Some(stroke_brush) = default_stroke_brush.clone() {
828                    draw_glyphs(
829                        item_renderer,
830                        run.font(),
831                        PhysicalLength::new(run.font_size()),
832                        normalized_coords,
833                        &synthesis,
834                        stroke_brush,
835                        para_y,
836                        &mut glyphs.into_iter(),
837                    );
838                }
839            }
840            None => {
841                draw_glyphs(
842                    item_renderer,
843                    run.font(),
844                    PhysicalLength::new(run.font_size()),
845                    normalized_coords,
846                    &synthesis,
847                    fill_brush.clone(),
848                    para_y,
849                    glyphs_it,
850                );
851            }
852        }
853
854        let metrics = run.metrics();
855
856        if glyph_run.style().underline.is_some() {
857            item_renderer.fill_rectangle(
858                PhysicalRect::new(
859                    PhysicalPoint::from_lengths(
860                        PhysicalLength::new(glyph_run.offset()),
861                        para_y
862                            + PhysicalLength::new(glyph_run.baseline() - metrics.underline_offset),
863                    ),
864                    PhysicalSize::new(glyph_run.advance(), metrics.underline_size),
865                ),
866                fill_brush.clone(),
867            );
868        }
869
870        if glyph_run.style().strikethrough.is_some() {
871            item_renderer.fill_rectangle(
872                PhysicalRect::new(
873                    PhysicalPoint::from_lengths(
874                        PhysicalLength::new(glyph_run.offset()),
875                        para_y
876                            + PhysicalLength::new(
877                                glyph_run.baseline() - metrics.strikethrough_offset,
878                            ),
879                    ),
880                    PhysicalSize::new(glyph_run.advance(), metrics.strikethrough_size),
881                ),
882                fill_brush,
883            );
884        }
885    }
886}
887
888/// Where `overflow: elide` cuts text off, computed across all paragraphs (each explicit `\n`
889/// produces one paragraph). See [`Layout::elision_extent`].
890#[derive(Clone, Copy)]
891struct ElisionCut {
892    /// Paragraph holding the last kept line.
893    last_paragraph: usize,
894    /// Last kept line within `last_paragraph`.
895    last_line: usize,
896    /// A line below the kept one was dropped for the height, so the kept line shows an ellipsis.
897    needs_ellipsis: bool,
898}
899
900struct Layout {
901    paragraphs: Vec<TextParagraph>,
902    y_offset: PhysicalLength,
903    max_width: PhysicalLength,
904    height: PhysicalLength,
905    max_physical_height: Option<PhysicalLength>,
906    elision_info: Option<ElisionInfo>,
907}
908
909impl Layout {
910    /// Returns true if the very first line is taller than the available height, meaning the
911    /// vertical line dropping used for `overflow: elide` would discard it and render nothing.
912    /// In that case the caller keeps drawing the first line but applies a hard pixel clip to
913    /// trim its vertical overflow, so it is shown (clipped) rather than disappearing entirely.
914    fn first_line_exceeds_height(&self) -> bool {
915        let Some(max_physical_height) = self.max_physical_height else {
916            return false;
917        };
918        self.paragraphs.first().and_then(|paragraph| paragraph.layout.lines().next()).is_some_and(
919            |line| !line_fits_height(line.metrics().block_max_coord, max_physical_height),
920        )
921    }
922
923    /// Whether a line of `paragraph` (with the metrics block range `block_min`..`block_max` in the
924    /// paragraph's local coordinates) falls within the box for `overflow: elide` with a height
925    /// limit. Accounts for vertical alignment via `y_offset`, which is negative for bottom/center
926    /// alignment. Without a height limit, or when not eliding, every line counts as within the box.
927    fn paragraph_line_within_box(
928        &self,
929        paragraph: &TextParagraph,
930        block_min: f32,
931        block_max: f32,
932    ) -> bool {
933        match self.max_physical_height {
934            Some(max_physical_height) if self.elision_info.is_some() => {
935                let para_y = self.y_offset + paragraph.y;
936                // `line_fits_height` rounds the bottom up by a pixel; allow the same slack at the
937                // top so a line sitting right on the box edge isn't dropped to a rounding error.
938                line_fits_height(para_y.get() + block_max, max_physical_height)
939                    && para_y.get() + block_min >= -0.5
940            }
941            _ => true,
942        }
943    }
944
945    /// For `overflow: elide` with a height limit, work out the last line to keep across all
946    /// paragraphs. Explicit `\n` line breaks each produce a paragraph, and they have to elide as a
947    /// single block: lines below the box are dropped and the ellipsis goes on the last visible
948    /// line. Returns `None` when there is no height limit or elision (draw everything). When
949    /// nothing fits at all the very first line is kept (#12197) so the text never vanishes
950    /// entirely; `draw_text` then clips its vertical overflow.
951    fn elision_extent(&self) -> Option<ElisionCut> {
952        self.max_physical_height?;
953        self.elision_info.as_ref()?;
954
955        // The deepest line still within the box, scanning paragraphs and their lines from the
956        // bottom up. Bottom/center alignment clips lines off the top, so the visible block can
957        // start partway down, but its last line is always the lowest one that fits.
958        let last_within_box = self.paragraphs.iter().enumerate().rev().find_map(|(pi, para)| {
959            para.layout
960                .lines()
961                .enumerate()
962                .rev()
963                .find(|(_, line)| {
964                    let m = line.metrics();
965                    self.paragraph_line_within_box(para, m.block_min_coord, m.block_max_coord)
966                })
967                .map(|(li, _)| (pi, li))
968        });
969
970        // The very last line in document order, used to tell whether anything was dropped below
971        // the kept line (and so whether an ellipsis is needed).
972        let final_line = self
973            .paragraphs
974            .iter()
975            .enumerate()
976            .rev()
977            .find_map(|(pi, para)| para.layout.lines().len().checked_sub(1).map(|li| (pi, li)));
978
979        let (last_paragraph, last_line) = last_within_box.unwrap_or((0, 0));
980        let needs_ellipsis =
981            final_line.is_some_and(|final_line| final_line != (last_paragraph, last_line));
982        Some(ElisionCut { last_paragraph, last_line, needs_ellipsis })
983    }
984
985    /// Returns the last paragraph starting at or before the given byte offset. An offset in the
986    /// gap between two paragraph ranges (between a '\r' and its '\n') thus maps to the preceding
987    /// paragraph; callers have to clamp their local offset to the paragraph's range.
988    fn paragraph_by_byte_offset(&self, byte_offset: usize) -> Option<&TextParagraph> {
989        self.paragraphs.iter().take_while(|p| p.range.start <= byte_offset).last()
990    }
991
992    fn paragraph_by_y(&self, y: PhysicalLength) -> Option<&TextParagraph> {
993        // Adjust for vertical alignment
994        let y = y - self.y_offset;
995
996        if y < PhysicalLength::zero() {
997            return self.paragraphs.first();
998        }
999
1000        let idx = self.paragraphs.binary_search_by(|paragraph| {
1001            if y < paragraph.y {
1002                core::cmp::Ordering::Greater
1003            } else if y >= paragraph.y + PhysicalLength::new(paragraph.layout.height()) {
1004                core::cmp::Ordering::Less
1005            } else {
1006                core::cmp::Ordering::Equal
1007            }
1008        });
1009
1010        match idx {
1011            Ok(i) => self.paragraphs.get(i),
1012            Err(_) => self.paragraphs.last(),
1013        }
1014    }
1015
1016    fn selection_geometry(
1017        &self,
1018        selection_range: Range<usize>,
1019        mut callback: impl FnMut(PhysicalRect),
1020    ) {
1021        for paragraph in &self.paragraphs {
1022            let selection_start = selection_range.start.max(paragraph.range.start);
1023            let selection_end = selection_range.end.min(paragraph.range.end);
1024
1025            if selection_start < selection_end {
1026                let local_start = selection_start - paragraph.range.start;
1027                let local_end = selection_end - paragraph.range.start;
1028
1029                let selection = parley::editing::Selection::new(
1030                    parley::editing::Cursor::from_byte_index(
1031                        &paragraph.layout,
1032                        local_start,
1033                        Default::default(),
1034                    ),
1035                    parley::editing::Cursor::from_byte_index(
1036                        &paragraph.layout,
1037                        local_end,
1038                        Default::default(),
1039                    ),
1040                );
1041
1042                selection.geometry_with(&paragraph.layout, |rect, _| {
1043                    callback(PhysicalRect::new(
1044                        PhysicalPoint::from_lengths(
1045                            PhysicalLength::new(rect.x0 as _),
1046                            PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
1047                        ),
1048                        PhysicalSize::new(rect.width() as _, rect.height() as _),
1049                    ));
1050                });
1051            }
1052        }
1053    }
1054
1055    fn byte_offset_from_point(&self, pos: PhysicalPoint) -> usize {
1056        let Some(paragraph) = self.paragraph_by_y(pos.y_length()) else {
1057            return 0;
1058        };
1059        let cursor = parley::editing::Cursor::from_point(
1060            &paragraph.layout,
1061            pos.x,
1062            (pos.y_length() - self.y_offset - paragraph.y).get(),
1063        );
1064        paragraph.range.start + cursor.index()
1065    }
1066
1067    fn cursor_rect_for_byte_offset(
1068        &self,
1069        byte_offset: usize,
1070        cursor_width: PhysicalLength,
1071    ) -> PhysicalRect {
1072        let Some(paragraph) = self.paragraph_by_byte_offset(byte_offset) else {
1073            return PhysicalRect::new(PhysicalPoint::default(), PhysicalSize::new(1.0, 1.0));
1074        };
1075
1076        let local_offset = (byte_offset - paragraph.range.start).min(paragraph.range.len());
1077        let cursor = parley::editing::Cursor::from_byte_index(
1078            &paragraph.layout,
1079            local_offset,
1080            Default::default(),
1081        );
1082        let rect = cursor.geometry(&paragraph.layout, cursor_width.get());
1083
1084        PhysicalRect::new(
1085            PhysicalPoint::from_lengths(
1086                PhysicalLength::new(rect.x0 as _),
1087                PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
1088            ),
1089            PhysicalSize::new(rect.width() as _, rect.height() as _),
1090        )
1091    }
1092
1093    /// Returns an iterator over the run's glyphs, truncated if necessary to fit within the max width,
1094    /// plus an optional ellipsis glyph with its font and size to be drawn separately.
1095    /// Call this function only for the last line of the layout.
1096    fn glyphs_with_elision<'a>(
1097        &'a self,
1098        glyph_run: &'a parley::layout::GlyphRun<Brush>,
1099        // When set, place an ellipsis even if the run fits the width. Used when lines below were
1100        // dropped for the height, so the last visible line signals the vertical truncation.
1101        force_elision: bool,
1102        // Advance width of the line's trailing whitespace. A vertically truncated line that fits
1103        // the width anchors the appended ellipsis after the last non-whitespace glyph, so trailing
1104        // spaces (e.g. left at a word-wrap break) don't push it away from the text.
1105        trailing_whitespace: f32,
1106    ) -> (
1107        impl Iterator<Item = parley::layout::Glyph> + Clone + 'a,
1108        Option<(parley::layout::Glyph, parley::FontData, PhysicalLength)>,
1109    ) {
1110        let ellipsis_advance =
1111            self.elision_info.as_ref().map(|info| info.ellipsis_glyph.advance).unwrap_or(0.0);
1112        let max_width = self
1113            .elision_info
1114            .as_ref()
1115            .map(|info| info.max_physical_width)
1116            .unwrap_or(PhysicalLength::new(f32::MAX));
1117
1118        let run_start = PhysicalLength::new(glyph_run.offset());
1119        let run_end = PhysicalLength::new(glyph_run.offset() + glyph_run.advance());
1120
1121        // Run starts after where the ellipsis would go - skip entirely
1122        let run_beyond_elision = run_start > max_width;
1123        // Run extends beyond max width (or the lines below it were dropped) and needs an ellipsis
1124        let needs_elision = !run_beyond_elision
1125            && (force_elision || run_end.get().floor() > max_width.get().ceil());
1126
1127        let truncated_glyphs = glyph_run.positioned_glyphs().take_while(move |glyph| {
1128            !run_beyond_elision
1129                && (!needs_elision
1130                    || PhysicalLength::new(glyph.x + glyph.advance + ellipsis_advance) <= max_width)
1131        });
1132
1133        let ellipsis = if needs_elision {
1134            self.elision_info.as_ref().map(|info| {
1135                let ellipsis_x = glyph_run
1136                    .positioned_glyphs()
1137                    .find(|glyph| {
1138                        PhysicalLength::new(glyph.x + glyph.advance + info.ellipsis_glyph.advance)
1139                            > info.max_physical_width
1140                    })
1141                    .map(|g| g.x)
1142                    // Nothing overflows horizontally (force_elision): put the ellipsis right after
1143                    // the run's last non-whitespace glyph, i.e. before any trailing whitespace.
1144                    .unwrap_or(run_end.get() - trailing_whitespace);
1145
1146                let mut ellipsis_glyph = info.ellipsis_glyph;
1147                ellipsis_glyph.x = ellipsis_x;
1148                // The ellipsis glyph comes from a standalone layout; place it on this run's
1149                // baseline so it lands on the right line (not just the first one).
1150                ellipsis_glyph.y = glyph_run.baseline();
1151
1152                let font_size = PhysicalLength::new(glyph_run.run().font_size());
1153                (ellipsis_glyph, info.font_for_ellipsis_glyph.clone(), font_size)
1154            })
1155        } else {
1156            None
1157        };
1158
1159        (truncated_glyphs, ellipsis)
1160    }
1161
1162    fn draw<R: GlyphRenderer>(
1163        &self,
1164        item_renderer: &mut R,
1165        default_fill_brush: <R as GlyphRenderer>::PlatformBrush,
1166        default_stroke_brush: Option<<R as GlyphRenderer>::PlatformBrush>,
1167        draw_glyphs: &mut dyn FnMut(
1168            &mut R,
1169            &parley::FontData,
1170            PhysicalLength,
1171            &[i16],               // normalized variation coords
1172            &fontique::Synthesis, // design-space variation settings
1173            <R as GlyphRenderer>::PlatformBrush,
1174            PhysicalLength, // y offset for paragraph
1175            &mut dyn Iterator<Item = parley::layout::Glyph>,
1176        ),
1177    ) {
1178        // Compute the elision cut once: explicit `\n` breaks produce one paragraph each, but they
1179        // must elide as a single block (drop lines below the box, ellipsis on the last visible one).
1180        let elision_extent = self.elision_extent();
1181        for (paragraph_index, paragraph) in self.paragraphs.iter().enumerate() {
1182            paragraph.draw(
1183                self,
1184                paragraph_index,
1185                elision_extent,
1186                item_renderer,
1187                &default_fill_brush,
1188                &default_stroke_brush,
1189                draw_glyphs,
1190            );
1191        }
1192    }
1193}
1194
1195pub fn draw_text(
1196    item_renderer: &mut impl GlyphRenderer,
1197    text: Pin<&dyn crate::item_rendering::RenderText>,
1198    item_rc: Option<&crate::item_tree::ItemRc>,
1199    size: LogicalSize,
1200    cache: Option<&TextLayoutCache>,
1201) {
1202    let max_width = size.width_length();
1203    let max_height = size.height_length();
1204
1205    if max_width.get() <= 0. || max_height.get() <= 0. {
1206        return;
1207    }
1208
1209    let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size)
1210    else {
1211        // Nothing to draw
1212        return;
1213    };
1214
1215    let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1216
1217    let (stroke_brush, stroke_width, stroke_style) = text.stroke();
1218    let platform_stroke_brush = if !stroke_brush.is_transparent() {
1219        let stroke_width = if stroke_width.get() != 0.0 {
1220            (stroke_width * scale_factor).get()
1221        } else {
1222            // Hairline stroke
1223            1.0
1224        };
1225        let stroke_width = match stroke_style {
1226            TextStrokeStyle::Outside => stroke_width * 2.0,
1227            TextStrokeStyle::Center => stroke_width,
1228        };
1229        item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size)
1230    } else {
1231        None
1232    };
1233
1234    // The layout_builder is still needed for the elision glyph in layout().
1235    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1236        item_rc.map(|item_rc| text.font_request(item_rc)),
1237        text.wrap(),
1238        platform_stroke_brush.is_some().then_some(stroke_style),
1239        scale_factor,
1240    );
1241
1242    let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1243
1244    let mut guard =
1245        get_or_create_text_paragraphs(cache, item_rc, text, scale_factor, &mut font_ctx);
1246
1247    let (horizontal_align, vertical_align) = text.alignment();
1248    let text_overflow = text.overflow();
1249
1250    let layout = layout(
1251        &layout_builder,
1252        &mut font_ctx,
1253        guard.paragraphs.take().unwrap_or_default(),
1254        scale_factor,
1255        LayoutOptions {
1256            horizontal_align,
1257            vertical_align,
1258            max_height: Some(max_height),
1259            max_width: Some(max_width),
1260            text_overflow: text.overflow(),
1261        },
1262    );
1263
1264    drop(font_ctx);
1265
1266    // When `overflow: elide` can't even fit the first line, the line is still drawn (rather than
1267    // dropped, which would render nothing) but its vertical overflow needs to be clipped like
1268    // `overflow: clip` would. Horizontal elision still applies, so a line that is both too tall
1269    // and too wide is clipped vertically and gets an ellipsis horizontally.
1270    let clip_overflowing_first_line =
1271        text_overflow == TextOverflow::Elide && layout.first_line_exceeds_height();
1272
1273    let render = if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
1274        item_renderer.save_state();
1275
1276        item_renderer.combine_clip(
1277            LogicalRect::new(LogicalPoint::default(), size),
1278            LogicalBorderRadius::zero(),
1279            LogicalLength::zero(),
1280        )
1281    } else {
1282        true
1283    };
1284
1285    if render {
1286        layout.draw(
1287            item_renderer,
1288            platform_fill_brush,
1289            platform_stroke_brush,
1290            &mut |item_renderer: &mut _,
1291                  font,
1292                  font_size,
1293                  normalized_coords,
1294                  synthesis,
1295                  brush,
1296                  y_offset,
1297                  glyphs_it| {
1298                item_renderer.draw_glyph_run(
1299                    font,
1300                    font_size,
1301                    normalized_coords,
1302                    synthesis,
1303                    brush,
1304                    y_offset,
1305                    glyphs_it,
1306                );
1307            },
1308        );
1309    }
1310
1311    if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
1312        item_renderer.restore_state();
1313    }
1314
1315    // Put paragraphs back into the cache guard for reuse.
1316    // break_all_lines replaces line data each time, so the state is ready for the next call.
1317    guard.paragraphs = Some(layout.paragraphs);
1318}
1319
1320#[cfg(feature = "std")]
1321pub fn link_under_cursor(
1322    font_context: &mut parley::FontContext,
1323    scale_factor: ScaleFactor,
1324    text: Pin<&dyn crate::item_rendering::RenderText>,
1325    item_rc: &crate::item_tree::ItemRc,
1326    size: LogicalSize,
1327    cursor: PhysicalPoint,
1328    cache: Option<&TextLayoutCache>,
1329) -> Option<std::string::String> {
1330    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1331        Some(text.font_request(item_rc)),
1332        text.wrap(),
1333        None,
1334        scale_factor,
1335    );
1336
1337    let mut guard =
1338        get_or_create_text_paragraphs(cache, Some(item_rc), text, scale_factor, font_context);
1339
1340    let (horizontal_align, vertical_align) = text.alignment();
1341
1342    let layout = layout(
1343        &layout_builder,
1344        font_context,
1345        guard.paragraphs.take().unwrap_or_default(),
1346        scale_factor,
1347        LayoutOptions {
1348            horizontal_align,
1349            vertical_align,
1350            max_height: Some(size.height_length()),
1351            max_width: Some(size.width_length()),
1352            text_overflow: text.overflow(),
1353        },
1354    );
1355
1356    let result = layout.paragraph_by_y(cursor.y_length()).and_then(|paragraph| {
1357        let paragraph_y: f64 = paragraph.y.cast::<f64>().get();
1358
1359        paragraph
1360            .links
1361            .iter()
1362            .find(|(range, _)| {
1363                let start = parley::editing::Cursor::from_byte_index(
1364                    &paragraph.layout,
1365                    range.start,
1366                    Default::default(),
1367                );
1368                let end = parley::editing::Cursor::from_byte_index(
1369                    &paragraph.layout,
1370                    range.end,
1371                    Default::default(),
1372                );
1373                let mut clicked = false;
1374                let link_range = parley::Selection::new(start, end);
1375                link_range.geometry_with(&paragraph.layout, |mut bounding_box, _line| {
1376                    bounding_box.y0 += paragraph_y;
1377                    bounding_box.y1 += paragraph_y;
1378                    clicked = bounding_box.union(parley::BoundingBox::new(
1379                        cursor.x.into(),
1380                        cursor.y.into(),
1381                        cursor.x.into(),
1382                        cursor.y.into(),
1383                    )) == bounding_box;
1384                });
1385                clicked
1386            })
1387            .map(|(_, link)| link.clone())
1388    });
1389
1390    // Put paragraphs back into the cache guard for reuse.
1391    guard.paragraphs = Some(layout.paragraphs);
1392
1393    result
1394}
1395
1396pub fn draw_text_input(
1397    item_renderer: &mut impl GlyphRenderer,
1398    text_input: Pin<&crate::items::TextInput>,
1399    item_rc: &crate::item_tree::ItemRc,
1400    size: LogicalSize,
1401    password_character: Option<fn() -> char>,
1402) {
1403    let width = size.width_length();
1404    let height = size.height_length();
1405    if width.get() <= 0. || height.get() <= 0. {
1406        return;
1407    }
1408
1409    let visual_representation = text_input.visual_representation(password_character);
1410
1411    let Some(platform_fill_brush) =
1412        item_renderer.platform_text_fill_brush(visual_representation.text_color, size)
1413    else {
1414        return;
1415    };
1416
1417    let selection_range = if !visual_representation.preedit_range.is_empty() {
1418        visual_representation.preedit_range.start..visual_representation.preedit_range.end
1419    } else {
1420        visual_representation.selection_range.start..visual_representation.selection_range.end
1421    };
1422
1423    let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1424
1425    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1426        Some(text_input.font_request(item_rc)),
1427        text_input.wrap(),
1428        None,
1429        scale_factor,
1430    );
1431
1432    let text = visual_representation.text.clone();
1433
1434    // When a piece of text is first selected, it gets an empty range like `Some(1..1)`.
1435    // If the text starts with a multi-byte character then this selection will be within
1436    // that character and parley will panic. We just filter out empty selection ranges.
1437    let selection_and_color = if !selection_range.is_empty() {
1438        Some((selection_range.clone(), text_input.selection_foreground_color()))
1439    } else {
1440        None
1441    };
1442
1443    let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1444
1445    let paragraphs_without_linebreaks = create_text_paragraphs(
1446        &layout_builder,
1447        &mut font_ctx,
1448        PlainOrStyledText::Plain(text),
1449        selection_and_color,
1450        Color::default(),
1451    );
1452
1453    let layout = layout(
1454        &layout_builder,
1455        &mut font_ctx,
1456        paragraphs_without_linebreaks,
1457        scale_factor,
1458        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1459    );
1460
1461    drop(font_ctx);
1462
1463    layout.selection_geometry(selection_range, |selection_rect| {
1464        item_renderer
1465            .fill_rectangle_with_color(selection_rect, text_input.selection_background_color());
1466    });
1467
1468    item_renderer.save_state();
1469
1470    let render = item_renderer.combine_clip(
1471        LogicalRect::new(LogicalPoint::default(), size),
1472        LogicalBorderRadius::zero(),
1473        LogicalLength::zero(),
1474    );
1475
1476    if render {
1477        layout.draw(
1478            item_renderer,
1479            platform_fill_brush,
1480            None,
1481            &mut |item_renderer: &mut _,
1482                  font,
1483                  font_size,
1484                  normalized_coords,
1485                  synthesis,
1486                  brush,
1487                  y_offset,
1488                  glyphs_it| {
1489                item_renderer.draw_glyph_run(
1490                    font,
1491                    font_size,
1492                    normalized_coords,
1493                    synthesis,
1494                    brush,
1495                    y_offset,
1496                    glyphs_it,
1497                );
1498            },
1499        );
1500
1501        if let Some(cursor_pos) = visual_representation.cursor_position {
1502            let cursor_rect = layout.cursor_rect_for_byte_offset(
1503                cursor_pos,
1504                text_input.text_cursor_width() * scale_factor,
1505            );
1506            item_renderer
1507                .fill_rectangle_with_color(cursor_rect, visual_representation.cursor_color);
1508        }
1509    }
1510
1511    item_renderer.restore_state();
1512}
1513
1514pub fn text_size(
1515    renderer: &dyn RendererSealed,
1516    text_item: Pin<&dyn crate::item_rendering::RenderString>,
1517    item_rc: &crate::item_tree::ItemRc,
1518    max_width: Option<LogicalLength>,
1519    text_wrap: TextWrap,
1520    _cache: Option<&TextLayoutCache>,
1521) -> Option<LogicalSize> {
1522    let scale_factor = renderer.scale_factor()?;
1523
1524    // Evaluate properties before borrowing font_context: both font_request()
1525    // and text() can trigger property bindings that re-enter text_size for
1526    // other elements, which would panic on a second borrow_mut().
1527    let font_request = text_item.font_request(item_rc);
1528    let text = text_item.text();
1529
1530    let ctx = renderer.slint_context()?;
1531    let mut font_ctx = ctx.font_context().borrow_mut();
1532
1533    let layout_builder =
1534        LayoutWithoutLineBreaksBuilder::new(Some(font_request), text_wrap, None, scale_factor);
1535
1536    let paragraphs_without_linebreaks =
1537        create_text_paragraphs(&layout_builder, &mut font_ctx, text, None, Color::default());
1538
1539    let layout = layout(
1540        &layout_builder,
1541        &mut font_ctx,
1542        paragraphs_without_linebreaks,
1543        scale_factor,
1544        LayoutOptions {
1545            max_width,
1546            max_height: None,
1547            horizontal_align: TextHorizontalAlignment::Left,
1548            vertical_align: TextVerticalAlignment::Top,
1549            text_overflow: TextOverflow::Clip,
1550        },
1551    );
1552    Some(PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor)
1553}
1554
1555pub fn char_size(
1556    font_ctx: &mut parley::FontContext,
1557    text_item: Pin<&dyn crate::item_rendering::HasFont>,
1558    item_rc: &crate::item_tree::ItemRc,
1559    ch: char,
1560) -> Option<LogicalSize> {
1561    let font_request = text_item.font_request(item_rc);
1562    let font = font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
1563
1564    let char_map = font.charmap()?;
1565
1566    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1567
1568    let glyph_index = char_map.map(ch)?;
1569
1570    let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE);
1571
1572    let location = face.axes().location(font.synthesis.variation_settings());
1573
1574    let glyph_metrics = skrifa::metrics::GlyphMetrics::new(
1575        &face,
1576        skrifa::instance::Size::new(pixel_size.get()),
1577        &location,
1578    );
1579
1580    let advance_width = LogicalLength::new(glyph_metrics.advance_width(glyph_index.into())?);
1581
1582    let font_metrics = skrifa::metrics::Metrics::new(
1583        &face,
1584        skrifa::instance::Size::new(pixel_size.get()),
1585        &location,
1586    );
1587
1588    Some(LogicalSize::from_lengths(
1589        advance_width,
1590        LogicalLength::new(font_metrics.ascent - font_metrics.descent),
1591    ))
1592}
1593
1594pub fn font_metrics(
1595    font_ctx: &mut parley::FontContext,
1596    font_request: FontRequest,
1597) -> crate::items::FontMetrics {
1598    let logical_pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE).get();
1599
1600    let Some(font) =
1601        font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)
1602    else {
1603        return crate::items::FontMetrics::default();
1604    };
1605
1606    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1607    let location = face.axes().location(font.synthesis.variation_settings());
1608    let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
1609
1610    let units_per_em = metrics.units_per_em as f32;
1611
1612    crate::items::FontMetrics {
1613        ascent: metrics.ascent * logical_pixel_size / units_per_em,
1614        descent: metrics.descent * logical_pixel_size / units_per_em,
1615        x_height: metrics.x_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1616        cap_height: metrics.cap_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1617    }
1618}
1619
1620pub fn text_input_byte_offset_for_position(
1621    renderer: &dyn RendererSealed,
1622    text_input: Pin<&crate::items::TextInput>,
1623    item_rc: &crate::item_tree::ItemRc,
1624    pos: LogicalPoint,
1625) -> usize {
1626    let Some(scale_factor) = renderer.scale_factor() else {
1627        return 0;
1628    };
1629    let pos: PhysicalPoint = pos * scale_factor;
1630
1631    let width = text_input.width();
1632    let height = text_input.height();
1633    if width.get() <= 0. || height.get() <= 0. || pos.y < 0. {
1634        return 0;
1635    }
1636
1637    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1638        Some(text_input.font_request(item_rc)),
1639        text_input.wrap(),
1640        None,
1641        scale_factor,
1642    );
1643    let visual_representation = text_input.visual_representation(None);
1644
1645    let Some(ctx) = renderer.slint_context() else {
1646        return 0;
1647    };
1648    let mut font_ctx = ctx.font_context().borrow_mut();
1649
1650    let paragraphs_without_linebreaks = create_text_paragraphs(
1651        &layout_builder,
1652        &mut font_ctx,
1653        PlainOrStyledText::Plain(visual_representation.text.clone()),
1654        None,
1655        Color::default(),
1656    );
1657
1658    let layout = layout(
1659        &layout_builder,
1660        &mut font_ctx,
1661        paragraphs_without_linebreaks,
1662        scale_factor,
1663        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1664    );
1665    let byte_offset = layout.byte_offset_from_point(pos);
1666    visual_representation.map_byte_offset_from_visual_text_to_actual_text(byte_offset)
1667}
1668
1669pub fn text_input_cursor_rect_for_byte_offset(
1670    renderer: &dyn RendererSealed,
1671    text_input: Pin<&crate::items::TextInput>,
1672    item_rc: &crate::item_tree::ItemRc,
1673    byte_offset: usize,
1674) -> LogicalRect {
1675    let Some(scale_factor) = renderer.scale_factor() else {
1676        return LogicalRect::default();
1677    };
1678
1679    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1680        Some(text_input.font_request(item_rc)),
1681        text_input.wrap(),
1682        None,
1683        scale_factor,
1684    );
1685
1686    let width = text_input.width();
1687    let height = text_input.height();
1688    if width.get() <= 0. || height.get() <= 0. {
1689        return LogicalRect::new(
1690            LogicalPoint::default(),
1691            LogicalSize::from_lengths(LogicalLength::new(1.0), layout_builder.pixel_size),
1692        );
1693    }
1694
1695    let visual_representation = text_input.visual_representation(None);
1696    let cursor_width = text_input.text_cursor_width() * scale_factor;
1697
1698    let Some(ctx) = renderer.slint_context() else {
1699        return LogicalRect::default();
1700    };
1701
1702    let mut font_ctx = ctx.font_context().borrow_mut();
1703
1704    let byte_offset = visual_representation.map_byte_offset_from_actual_to_visual_text(byte_offset);
1705
1706    let paragraphs_without_linebreaks = create_text_paragraphs(
1707        &layout_builder,
1708        &mut font_ctx,
1709        PlainOrStyledText::Plain(visual_representation.text),
1710        None,
1711        Color::default(),
1712    );
1713
1714    let layout = layout(
1715        &layout_builder,
1716        &mut font_ctx,
1717        paragraphs_without_linebreaks,
1718        scale_factor,
1719        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1720    );
1721    let cursor_rect = layout.cursor_rect_for_byte_offset(byte_offset, cursor_width);
1722    cursor_rect / scale_factor
1723}
1724
1725#[cfg(test)]
1726mod tests {
1727    use super::*;
1728
1729    fn paragraphs(text: &str) -> Vec<&str> {
1730        paragraph_ranges(text).map(|r| &text[r]).collect()
1731    }
1732
1733    fn layout_text(text: &str) -> Layout {
1734        // Don't load system fonts: that goes through fontconfig FFI, which Miri
1735        // can't execute. Use the bundled Inter font instead.
1736        let mut font_ctx = parley::FontContext {
1737            collection: fontique::Collection::new(fontique::CollectionOptions {
1738                system_fonts: false,
1739                ..Default::default()
1740            }),
1741            source_cache: Default::default(),
1742        };
1743        let data = include_bytes!("../../common/sharedfontique/Inter-VariableFont.ttf");
1744        let families =
1745            font_ctx.collection.register_fonts(fontique::Blob::new(Arc::new(data)), None);
1746        font_ctx.collection.set_generic_families(
1747            fontique::GenericFamily::SansSerif,
1748            families.iter().map(|(id, _)| *id),
1749        );
1750        let builder = LayoutWithoutLineBreaksBuilder::new(
1751            None,
1752            TextWrap::NoWrap,
1753            None,
1754            ScaleFactor::new(1.0),
1755        );
1756        let paragraphs = create_text_paragraphs(
1757            &builder,
1758            &mut font_ctx,
1759            PlainOrStyledText::Plain(text.into()),
1760            None,
1761            Color::default(),
1762        );
1763        layout(&builder, &mut font_ctx, paragraphs, ScaleFactor::new(1.0), LayoutOptions::default())
1764    }
1765
1766    fn visual_line_count(text: &str) -> usize {
1767        layout_text(text).paragraphs.iter().map(|p| p.layout.lines().len()).sum()
1768    }
1769
1770    #[test]
1771    fn test_crlf_line_count() {
1772        assert_eq!(visual_line_count("hello\r\nworld"), visual_line_count("hello\nworld"));
1773        assert_eq!(visual_line_count("hello\r\nworld"), 2);
1774    }
1775
1776    #[test]
1777    fn test_cursor_between_cr_and_lf() {
1778        // The cursor can land between the '\r' and the '\n' (e.g. moving left from the start of
1779        // the next line); it draws at the end of the preceding paragraph, like on the '\r'.
1780        let layout = layout_text("hello\r\nworld");
1781        let cursor_width = PhysicalLength::new(1.0);
1782        assert_eq!(
1783            layout.cursor_rect_for_byte_offset(6, cursor_width),
1784            layout.cursor_rect_for_byte_offset(5, cursor_width)
1785        );
1786        assert_ne!(
1787            layout.cursor_rect_for_byte_offset(6, cursor_width),
1788            layout.cursor_rect_for_byte_offset(0, cursor_width)
1789        );
1790    }
1791
1792    #[test]
1793    fn test_paragraph_ranges() {
1794        assert_eq!(paragraphs(""), [""]);
1795        assert_eq!(paragraphs("hello"), ["hello"]);
1796        assert_eq!(paragraphs("hello\nworld"), ["hello", "world"]);
1797        assert_eq!(paragraphs("hello\n"), ["hello", ""]);
1798        assert_eq!(paragraphs("\n\n"), ["", "", ""]);
1799    }
1800
1801    #[test]
1802    fn test_paragraph_ranges_crlf() {
1803        assert_eq!(paragraphs("hello\r\nworld"), ["hello", "world"]);
1804        assert_eq!(paragraphs("hello\r\n"), ["hello", ""]);
1805        assert_eq!(paragraphs("\r\n\r\n"), ["", "", ""]);
1806        assert_eq!(paragraphs("a\r\n\nb"), ["a", "", "b"]);
1807        // A lone CR stays in the paragraph; parley breaks the line there.
1808        assert_eq!(paragraphs("hello\rworld"), ["hello\rworld"]);
1809    }
1810}