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
169struct LayoutOptions {
170    max_width: Option<LogicalLength>,
171    max_height: Option<LogicalLength>,
172    horizontal_align: TextHorizontalAlignment,
173    vertical_align: TextVerticalAlignment,
174    text_overflow: TextOverflow,
175}
176
177impl LayoutOptions {
178    fn new_from_textinput(
179        text_input: Pin<&crate::items::TextInput>,
180        max_width: Option<LogicalLength>,
181        max_height: Option<LogicalLength>,
182    ) -> Self {
183        Self {
184            max_width,
185            max_height,
186            horizontal_align: text_input.horizontal_alignment(),
187            vertical_align: text_input.vertical_alignment(),
188            text_overflow: TextOverflow::Clip,
189        }
190    }
191}
192
193struct LayoutWithoutLineBreaksBuilder {
194    font_request: Option<FontRequest>,
195    text_wrap: TextWrap,
196    stroke: Option<TextStrokeStyle>,
197    scale_factor: ScaleFactor,
198    pixel_size: LogicalLength,
199}
200
201impl LayoutWithoutLineBreaksBuilder {
202    fn new(
203        font_request: Option<FontRequest>,
204        text_wrap: TextWrap,
205        stroke: Option<TextStrokeStyle>,
206        scale_factor: ScaleFactor,
207    ) -> Self {
208        let pixel_size = font_request
209            .as_ref()
210            .and_then(|font_request| font_request.pixel_size)
211            .unwrap_or(DEFAULT_FONT_SIZE);
212
213        Self { font_request, text_wrap, stroke, scale_factor, pixel_size }
214    }
215
216    fn ranged_builder<'a>(
217        &self,
218        layout_ctx: &'a mut parley::LayoutContext<Brush>,
219        font_ctx: &'a mut parley::FontContext,
220        text: &'a str,
221    ) -> parley::RangedBuilder<'a, Brush> {
222        // Pin the line height to the requested font's metrics so fallback runs (e.g.
223        // password chars rendered by a wider-descender symbol font) don't grow the
224        // line box. FontSizeRelative makes the ratio scale with any per-span FontSize.
225        let line_height_ratio = self.font_request.as_ref().and_then(|font_request| {
226            let font = font_request
227                .clone()
228                .query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
229            let face = skrifa::FontRef::from_index(font.blob.data(), font.index).ok()?;
230            let location = face.axes().location(font.synthesis.variation_settings());
231            let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
232            let units_per_em = metrics.units_per_em as f32;
233            (units_per_em > 0.0)
234                .then(|| (metrics.ascent - metrics.descent + metrics.leading) / units_per_em)
235        });
236
237        let mut builder = layout_ctx.ranged_builder(font_ctx, text, self.scale_factor.get(), false);
238
239        if let Some(ratio) = line_height_ratio {
240            builder.push_default(parley::StyleProperty::LineHeight(
241                parley::style::LineHeight::FontSizeRelative(ratio),
242            ));
243        }
244
245        if let Some(ref font_request) = self.font_request {
246            let mut fallback_family_iter = sharedfontique::FALLBACK_FAMILIES
247                .into_iter()
248                .map(parley::style::FontFamilyName::Generic);
249
250            let font_families: &[parley::style::FontFamilyName] = if let Some(family) =
251                &font_request.family
252            {
253                let mut iter =
254                    core::iter::once(parley::style::FontFamilyName::named(family.as_str()))
255                        .chain(fallback_family_iter);
256                &core::array::from_fn::<
257                    _,
258                    { sharedfontique::FALLBACK_FAMILIES.as_slice().len() + 1 },
259                    _,
260                >(|_| iter.next().unwrap())
261            } else {
262                &core::array::from_fn::<_, { sharedfontique::FALLBACK_FAMILIES.as_slice().len() }, _>(
263                    |_| fallback_family_iter.next().unwrap(),
264                )
265            };
266
267            builder.push_default(parley::style::FontFamily::List(std::borrow::Cow::Borrowed(
268                font_families,
269            )));
270
271            if let Some(weight) = font_request.weight {
272                builder.push_default(parley::StyleProperty::FontWeight(
273                    parley::style::FontWeight::new(weight as f32),
274                ));
275            }
276            if let Some(letter_spacing) = font_request.letter_spacing {
277                builder.push_default(parley::StyleProperty::LetterSpacing(letter_spacing.get()));
278            }
279            builder.push_default(parley::StyleProperty::FontStyle(if font_request.italic {
280                parley::style::FontStyle::Italic
281            } else {
282                parley::style::FontStyle::Normal
283            }));
284        }
285        builder.push_default(parley::StyleProperty::FontSize(self.pixel_size.get()));
286        builder.push_default(parley::StyleProperty::WordBreak(match self.text_wrap {
287            TextWrap::NoWrap => parley::style::WordBreak::KeepAll,
288            TextWrap::WordWrap => parley::style::WordBreak::Normal,
289            TextWrap::CharWrap => parley::style::WordBreak::BreakAll,
290        }));
291        builder.push_default(parley::StyleProperty::OverflowWrap(match self.text_wrap {
292            TextWrap::NoWrap => parley::style::OverflowWrap::Normal,
293            TextWrap::WordWrap | TextWrap::CharWrap => parley::style::OverflowWrap::Anywhere,
294        }));
295        if self.text_wrap == TextWrap::NoWrap {
296            // Parley 0.9 removed the width parameter from `Layout::align()` and instead
297            // uses the `max_advance` set by `break_all_lines()` as the alignment container
298            // width. To allow passing `max_physical_width` to `break_all_lines` for alignment
299            // purposes without triggering actual line wrapping, we must set `TextWrapMode::NoWrap`.
300            builder.push_default(parley::StyleProperty::TextWrapMode(
301                parley::style::TextWrapMode::NoWrap,
302            ));
303        }
304
305        builder.push_default(parley::StyleProperty::Brush(Brush {
306            override_fill_color: None,
307            stroke: self.stroke,
308            link_color: None,
309        }));
310
311        builder
312    }
313
314    fn build(
315        &self,
316        font_context: &mut parley::FontContext,
317        text: &str,
318        selection: Option<(Range<usize>, Color)>,
319        formatting: impl IntoIterator<Item = i_slint_common::styled_text::FormattedSpan>,
320        link_color: Option<Color>,
321    ) -> parley::Layout<Brush> {
322        use i_slint_common::styled_text::Style;
323
324        LAYOUT_CONTEXT.with_borrow_mut(|layout_ctx| {
325            let mut builder = self.ranged_builder(layout_ctx, font_context, text);
326
327            if let Some((selection_range, selection_color)) = selection {
328                {
329                    builder.push(
330                        parley::StyleProperty::Brush(Brush {
331                            override_fill_color: Some(selection_color),
332                            stroke: self.stroke,
333                            link_color: None,
334                        }),
335                        selection_range,
336                    );
337                }
338            }
339
340            // filter empty ranges otherwise parley will panic on assert
341            for span in formatting.into_iter().filter(|s| !s.range.is_empty()) {
342                match span.style {
343                    Style::Emphasis => {
344                        builder.push(
345                            parley::StyleProperty::FontStyle(parley::style::FontStyle::Italic),
346                            span.range,
347                        );
348                    }
349                    Style::Strikethrough => {
350                        builder.push(parley::StyleProperty::Strikethrough(true), span.range);
351                    }
352                    Style::Strong => {
353                        builder.push(
354                            parley::StyleProperty::FontWeight(parley::style::FontWeight::BOLD),
355                            span.range,
356                        );
357                    }
358                    Style::Code => {
359                        builder.push(
360                            parley::StyleProperty::FontFamily(parley::style::FontFamily::Single(
361                                parley::style::FontFamilyName::Generic(
362                                    parley::style::GenericFamily::Monospace,
363                                ),
364                            )),
365                            span.range,
366                        );
367                    }
368                    Style::Underline => {
369                        builder.push(parley::StyleProperty::Underline(true), span.range);
370                    }
371                    Style::Link => {
372                        builder.push(parley::StyleProperty::Underline(true), span.range.clone());
373                        builder.push(
374                            parley::StyleProperty::Brush(Brush {
375                                override_fill_color: None,
376                                stroke: self.stroke,
377                                link_color,
378                            }),
379                            span.range,
380                        );
381                    }
382                    Style::Color(color) => {
383                        builder.push(
384                            parley::StyleProperty::Brush(Brush {
385                                override_fill_color: Some(crate::Color::from_argb_encoded(color)),
386                                stroke: self.stroke,
387                                link_color: None,
388                            }),
389                            span.range,
390                        );
391                    }
392                }
393            }
394
395            builder.build(text)
396        })
397    }
398}
399
400fn create_text_paragraphs(
401    layout_builder: &LayoutWithoutLineBreaksBuilder,
402    font_context: &mut parley::FontContext,
403    text: PlainOrStyledText,
404    selection: Option<(Range<usize>, Color)>,
405    link_color: Color,
406) -> Vec<TextParagraph> {
407    let paragraph_from_text =
408        |font_context: &mut parley::FontContext,
409         text: &str,
410         range: std::ops::Range<usize>,
411         formatting: Vec<i_slint_common::styled_text::FormattedSpan>,
412         links: Vec<(std::ops::Range<usize>, std::string::String)>| {
413            let selection = selection.clone().and_then(|(selection, selection_color)| {
414                let sel_start = selection.start.max(range.start);
415                let sel_end = selection.end.min(range.end);
416
417                if sel_start < sel_end {
418                    let local_selection = (sel_start - range.start)..(sel_end - range.start);
419                    Some((local_selection, selection_color))
420                } else {
421                    None
422                }
423            });
424
425            let layout =
426                layout_builder.build(font_context, text, selection, formatting, Some(link_color));
427
428            TextParagraph { range, y: PhysicalLength::default(), layout, links }
429        };
430
431    let mut paragraphs = Vec::with_capacity(1);
432
433    match text {
434        PlainOrStyledText::Plain(ref text) => {
435            let paragraph_ranges = core::iter::from_fn({
436                let mut start = 0;
437                let mut char_it = text.char_indices().peekable();
438                let mut eot = false;
439                move || {
440                    for (idx, ch) in char_it.by_ref() {
441                        if ch == '\n' {
442                            let next_range = start..idx;
443                            start = idx + ch.len_utf8();
444                            return Some(next_range);
445                        }
446                    }
447
448                    if eot {
449                        return None;
450                    }
451                    eot = true;
452                    Some(start..text.len())
453                }
454            });
455
456            for range in paragraph_ranges {
457                paragraphs.push(paragraph_from_text(
458                    font_context,
459                    &text[range.clone()],
460                    range,
461                    Default::default(),
462                    Default::default(),
463                ));
464            }
465        }
466        PlainOrStyledText::Styled(rich_text) => {
467            for paragraph in rich_text.paragraphs {
468                paragraphs.push(paragraph_from_text(
469                    font_context,
470                    &paragraph.text,
471                    0..0,
472                    paragraph.formatting,
473                    paragraph.links,
474                ));
475            }
476        }
477    };
478
479    paragraphs
480}
481
482/// Note: parley currently uses `WordBreak` while shaping via `analyze_text()`,
483/// so shaped paragraphs aren't identical across wrap modes. This is why `text_size()`
484/// doesn't use the `TextLayoutCache` — it would be incorrect to share cached paragraphs
485/// shaped with one wrap mode and reuse them with another.
486fn layout(
487    layout_builder: &LayoutWithoutLineBreaksBuilder,
488    font_context: &mut parley::FontContext,
489    mut paragraphs: Vec<TextParagraph>,
490    scale_factor: ScaleFactor,
491    options: LayoutOptions,
492) -> Layout {
493    let max_physical_width = options.max_width.map(|max_width| max_width * scale_factor);
494    let max_physical_height = options.max_height.map(|max_height| max_height * scale_factor);
495
496    // Returned None if failed to get the ellipsis glyph for some rare reason.
497    let get_ellipsis_glyph = |font_context: &mut parley::FontContext| {
498        let mut layout = layout_builder.build(font_context, "…", None, None, None);
499        layout.break_all_lines(None);
500        let line = layout.lines().next()?;
501        let item = line.items().next()?;
502        let run = match item {
503            parley::layout::PositionedLayoutItem::GlyphRun(run) => Some(run),
504            _ => return None,
505        }?;
506        let glyph = run.positioned_glyphs().next()?;
507        Some((glyph, run.run().font().clone()))
508    };
509
510    let elision_info = if let (TextOverflow::Elide, Some(max_physical_width)) =
511        (options.text_overflow, max_physical_width)
512    {
513        get_ellipsis_glyph(font_context).map(|(ellipsis_glyph, font_for_ellipsis_glyph)| {
514            ElisionInfo { ellipsis_glyph, font_for_ellipsis_glyph, max_physical_width }
515        })
516    } else {
517        None
518    };
519
520    let mut para_y = 0.0;
521    for para in paragraphs.iter_mut() {
522        para.layout.break_all_lines(max_physical_width.map(|width| width.get()));
523        para.layout.align(
524            match options.horizontal_align {
525                TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
526                    parley::Alignment::Left
527                }
528                TextHorizontalAlignment::Center => parley::Alignment::Center,
529                TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
530                    parley::Alignment::Right
531                }
532            },
533            parley::AlignmentOptions::default(),
534        );
535
536        para.y = PhysicalLength::new(para_y);
537        para_y += para.layout.height();
538    }
539
540    let max_width = paragraphs
541        .iter()
542        .map(|p| {
543            // The max width is used for the ellipsis computation when eliding text. We *want* to exclude whitespace
544            // for that, but we can't at the glyph run level, so the glyph runs always *do* include whitespace glyphs,
545            // and as such we must also accept the full width here including trailing whitespace, otherwise text with
546            // trailing whitespace will assigned a smaller width for rendering and thus the ellipsis will be placed.
547            PhysicalLength::new(p.layout.full_width())
548        })
549        .fold(PhysicalLength::zero(), PhysicalLength::max);
550    let height = paragraphs
551        .last()
552        .map_or(PhysicalLength::zero(), |p| p.y + PhysicalLength::new(p.layout.height()));
553
554    let y_offset = match (max_physical_height, options.vertical_align) {
555        (Some(max_height), TextVerticalAlignment::Center) => (max_height - height) / 2.0,
556        (Some(max_height), TextVerticalAlignment::Bottom) => max_height - height,
557        (None, _) | (Some(_), TextVerticalAlignment::Top) => PhysicalLength::new(0.0),
558    };
559
560    Layout { paragraphs, y_offset, elision_info, max_width, height, max_physical_height }
561}
562
563/// RAII guard: takes Vec out of the cache on creation, puts it back on drop.
564struct CachedParagraphsGuard<'a> {
565    paragraphs: Option<Vec<TextParagraph>>,
566    container: Option<std::cell::RefMut<'a, Vec<TextParagraph>>>,
567}
568
569impl Drop for CachedParagraphsGuard<'_> {
570    fn drop(&mut self) {
571        if let (Some(paragraphs), Some(container)) = (self.paragraphs.take(), &mut self.container) {
572            **container = paragraphs;
573        }
574    }
575}
576
577fn shape_paragraphs(
578    text: Pin<&dyn crate::item_rendering::RenderText>,
579    item_rc: Option<&crate::item_tree::ItemRc>,
580    scale_factor: ScaleFactor,
581    font_context: &mut parley::FontContext,
582) -> Vec<TextParagraph> {
583    let (stroke_brush, _, stroke_style) = text.stroke();
584    let has_stroke = !stroke_brush.is_transparent();
585    let builder = LayoutWithoutLineBreaksBuilder::new(
586        item_rc.map(|irc| text.font_request(irc)),
587        text.wrap(),
588        has_stroke.then_some(stroke_style),
589        scale_factor,
590    );
591    create_text_paragraphs(&builder, font_context, text.text(), None, text.link_color())
592}
593
594fn get_or_create_text_paragraphs<'a>(
595    cache: Option<&'a TextLayoutCache>,
596    item_rc: Option<&crate::item_tree::ItemRc>,
597    text: Pin<&dyn crate::item_rendering::RenderText>,
598    scale_factor: ScaleFactor,
599    font_context: &mut parley::FontContext,
600) -> CachedParagraphsGuard<'a> {
601    if let (Some(cache), Some(item_rc)) = (cache, item_rc) {
602        let mut entry = cache.inner.get_or_update_cache_entry_ref(item_rc, || {
603            #[cfg(feature = "testing")]
604            cache.cache_miss_count.set(cache.cache_miss_count.get() + 1);
605            shape_paragraphs(text, Some(item_rc), scale_factor, font_context)
606        });
607        let paragraphs = std::mem::take(&mut *entry);
608        CachedParagraphsGuard { paragraphs: Some(paragraphs), container: Some(entry) }
609    } else {
610        CachedParagraphsGuard {
611            paragraphs: Some(shape_paragraphs(text, item_rc, scale_factor, font_context)),
612            container: None,
613        }
614    }
615}
616
617struct ElisionInfo {
618    ellipsis_glyph: parley::layout::Glyph,
619    font_for_ellipsis_glyph: parley::FontData,
620    max_physical_width: PhysicalLength,
621}
622
623/// Whether a line whose bottom edge is at `block_max_coord` fits within `max_physical_height`,
624/// rounding the height up so a sub-pixel overflow still counts as fitting.
625fn line_fits_height(block_max_coord: f32, max_physical_height: PhysicalLength) -> bool {
626    max_physical_height.get().ceil() >= block_max_coord
627}
628
629struct TextParagraph {
630    range: Range<usize>,
631    y: PhysicalLength,
632    layout: parley::Layout<Brush>,
633    links: std::vec::Vec<(Range<usize>, std::string::String)>,
634}
635
636impl TextParagraph {
637    fn draw<R: GlyphRenderer>(
638        &self,
639        layout: &Layout,
640        item_renderer: &mut R,
641        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
642        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
643        draw_glyphs: &mut dyn FnMut(
644            &mut R,
645            &parley::FontData,
646            PhysicalLength,
647            &[i16],               // normalized variation coords
648            &fontique::Synthesis, // design-space variation settings
649            <R as GlyphRenderer>::PlatformBrush,
650            PhysicalLength, // y offset for paragraph
651            &mut dyn Iterator<Item = parley::layout::Glyph>,
652        ),
653    ) {
654        let para_y = layout.y_offset + self.y;
655
656        let total_lines = self.layout.lines().len();
657
658        // For `overflow: elide` with a height limit (`overflow: clip` applies a hard pixel clip
659        // instead), keep the lines that actually fall within the box, taking the vertical alignment
660        // into account: the layout shifts every line down by `para_y`, which is negative for
661        // bottom/center alignment. Comparing only a line's bottom to the height (ignoring `para_y`)
662        // kept the wrong lines -- bottom-aligned text showed the lines clipped off the top instead
663        // of the visible ones anchored at the bottom.
664        let line_within_box = |block_min: f32, block_max: f32| match layout.max_physical_height {
665            Some(max_physical_height) if layout.elision_info.is_some() => {
666                // `line_fits_height` rounds the bottom up by a pixel; allow the same slack at the
667                // top so a line sitting right on the box edge isn't dropped to a rounding error.
668                line_fits_height(para_y.get() + block_max, max_physical_height)
669                    && para_y.get() + block_min >= -0.5
670            }
671            _ => true,
672        };
673
674        // The last line within the box, or the first line if none fit (e.g. a single line taller
675        // than the box, #12197) so the text isn't dropped entirely -- `draw_text` clips its overflow.
676        let last_drawn = self
677            .layout
678            .lines()
679            .enumerate()
680            .filter(|(_, line)| {
681                let m = line.metrics();
682                line_within_box(m.block_min_coord, m.block_max_coord)
683            })
684            .map(|(index, _)| index)
685            .next_back()
686            .or(Some(0));
687
688        for (index, line) in self.layout.lines().enumerate() {
689            let metrics = line.metrics();
690            let last_line = Some(index) == last_drawn;
691            if !last_line && !line_within_box(metrics.block_min_coord, metrics.block_max_coord) {
692                continue;
693            }
694            // The last drawn line should show an ellipsis if real lines below it were dropped for
695            // the height, even when it fits the width.
696            let vertically_truncated = last_line && index + 1 < total_lines;
697            for item in line.items() {
698                match item {
699                    parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
700                        let ellipsis = if last_line {
701                            let (truncated_glyphs, ellipsis) =
702                                layout.glyphs_with_elision(&glyph_run, vertically_truncated);
703
704                            Self::draw_glyph_run(
705                                &glyph_run,
706                                item_renderer,
707                                default_fill_brush,
708                                default_stroke_brush,
709                                para_y,
710                                &mut truncated_glyphs.into_iter(),
711                                draw_glyphs,
712                            );
713                            ellipsis
714                        } else {
715                            Self::draw_glyph_run(
716                                &glyph_run,
717                                item_renderer,
718                                default_fill_brush,
719                                default_stroke_brush,
720                                para_y,
721                                &mut glyph_run.positioned_glyphs(),
722                                draw_glyphs,
723                            );
724                            None
725                        };
726
727                        if let Some((ellipsis_glyph, ellipsis_font, font_size)) = ellipsis {
728                            let run = glyph_run.run();
729                            draw_glyphs(
730                                item_renderer,
731                                &ellipsis_font,
732                                font_size,
733                                run.normalized_coords(),
734                                &run.synthesis(),
735                                default_fill_brush.clone(),
736                                para_y,
737                                &mut core::iter::once(ellipsis_glyph),
738                            );
739                        }
740                    }
741                    parley::PositionedLayoutItem::InlineBox(_inline_box) => {}
742                };
743            }
744        }
745    }
746
747    fn draw_glyph_run<R: GlyphRenderer>(
748        glyph_run: &parley::layout::GlyphRun<Brush>,
749        item_renderer: &mut R,
750        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
751        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
752        para_y: PhysicalLength,
753        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
754        draw_glyphs: &mut dyn FnMut(
755            &mut R,
756            &parley::FontData,
757            PhysicalLength,
758            &[i16],               // normalized variation coords
759            &fontique::Synthesis, // design-space variation settings
760            <R as GlyphRenderer>::PlatformBrush,
761            PhysicalLength,
762            &mut dyn Iterator<Item = parley::layout::Glyph>,
763        ),
764    ) {
765        let run = glyph_run.run();
766        let normalized_coords = run.normalized_coords();
767        let synthesis = run.synthesis();
768        let brush = &glyph_run.style().brush;
769
770        let (fill_brush, stroke_style) = match (brush.override_fill_color, brush.link_color) {
771            (Some(color), _) => {
772                let Some(selection_brush) = item_renderer.platform_brush_for_color(&color) else {
773                    return;
774                };
775                (selection_brush.clone(), &None)
776            }
777            (None, Some(color)) => {
778                let Some(link_brush) = item_renderer.platform_brush_for_color(&color) else {
779                    return;
780                };
781                (link_brush.clone(), &None)
782            }
783            (None, None) => (default_fill_brush.clone(), &brush.stroke),
784        };
785
786        match stroke_style {
787            Some(TextStrokeStyle::Outside) => {
788                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
789
790                if let Some(stroke_brush) = default_stroke_brush.clone() {
791                    draw_glyphs(
792                        item_renderer,
793                        run.font(),
794                        PhysicalLength::new(run.font_size()),
795                        normalized_coords,
796                        &synthesis,
797                        stroke_brush,
798                        para_y,
799                        &mut glyphs.iter().cloned(),
800                    );
801                }
802
803                draw_glyphs(
804                    item_renderer,
805                    run.font(),
806                    PhysicalLength::new(run.font_size()),
807                    normalized_coords,
808                    &synthesis,
809                    fill_brush.clone(),
810                    para_y,
811                    &mut glyphs.into_iter(),
812                );
813            }
814            Some(TextStrokeStyle::Center) => {
815                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
816
817                draw_glyphs(
818                    item_renderer,
819                    run.font(),
820                    PhysicalLength::new(run.font_size()),
821                    normalized_coords,
822                    &synthesis,
823                    fill_brush.clone(),
824                    para_y,
825                    &mut glyphs.iter().cloned(),
826                );
827
828                if let Some(stroke_brush) = default_stroke_brush.clone() {
829                    draw_glyphs(
830                        item_renderer,
831                        run.font(),
832                        PhysicalLength::new(run.font_size()),
833                        normalized_coords,
834                        &synthesis,
835                        stroke_brush,
836                        para_y,
837                        &mut glyphs.into_iter(),
838                    );
839                }
840            }
841            None => {
842                draw_glyphs(
843                    item_renderer,
844                    run.font(),
845                    PhysicalLength::new(run.font_size()),
846                    normalized_coords,
847                    &synthesis,
848                    fill_brush.clone(),
849                    para_y,
850                    glyphs_it,
851                );
852            }
853        }
854
855        let metrics = run.metrics();
856
857        if glyph_run.style().underline.is_some() {
858            item_renderer.fill_rectangle(
859                PhysicalRect::new(
860                    PhysicalPoint::from_lengths(
861                        PhysicalLength::new(glyph_run.offset()),
862                        para_y
863                            + PhysicalLength::new(glyph_run.baseline() - metrics.underline_offset),
864                    ),
865                    PhysicalSize::new(glyph_run.advance(), metrics.underline_size),
866                ),
867                fill_brush.clone(),
868            );
869        }
870
871        if glyph_run.style().strikethrough.is_some() {
872            item_renderer.fill_rectangle(
873                PhysicalRect::new(
874                    PhysicalPoint::from_lengths(
875                        PhysicalLength::new(glyph_run.offset()),
876                        para_y
877                            + PhysicalLength::new(
878                                glyph_run.baseline() - metrics.strikethrough_offset,
879                            ),
880                    ),
881                    PhysicalSize::new(glyph_run.advance(), metrics.strikethrough_size),
882                ),
883                fill_brush,
884            );
885        }
886    }
887}
888
889struct Layout {
890    paragraphs: Vec<TextParagraph>,
891    y_offset: PhysicalLength,
892    max_width: PhysicalLength,
893    height: PhysicalLength,
894    max_physical_height: Option<PhysicalLength>,
895    elision_info: Option<ElisionInfo>,
896}
897
898impl Layout {
899    /// Returns true if the very first line is taller than the available height, meaning the
900    /// vertical line dropping used for `overflow: elide` would discard it and render nothing.
901    /// In that case the caller keeps drawing the first line but applies a hard pixel clip to
902    /// trim its vertical overflow, so it is shown (clipped) rather than disappearing entirely.
903    fn first_line_exceeds_height(&self) -> bool {
904        let Some(max_physical_height) = self.max_physical_height else {
905            return false;
906        };
907        self.paragraphs.first().and_then(|paragraph| paragraph.layout.lines().next()).is_some_and(
908            |line| !line_fits_height(line.metrics().block_max_coord, max_physical_height),
909        )
910    }
911
912    fn paragraph_by_byte_offset(&self, byte_offset: usize) -> Option<&TextParagraph> {
913        self.paragraphs.iter().find(|p| byte_offset >= p.range.start && byte_offset <= p.range.end)
914    }
915
916    fn paragraph_by_y(&self, y: PhysicalLength) -> Option<&TextParagraph> {
917        // Adjust for vertical alignment
918        let y = y - self.y_offset;
919
920        if y < PhysicalLength::zero() {
921            return self.paragraphs.first();
922        }
923
924        let idx = self.paragraphs.binary_search_by(|paragraph| {
925            if y < paragraph.y {
926                core::cmp::Ordering::Greater
927            } else if y >= paragraph.y + PhysicalLength::new(paragraph.layout.height()) {
928                core::cmp::Ordering::Less
929            } else {
930                core::cmp::Ordering::Equal
931            }
932        });
933
934        match idx {
935            Ok(i) => self.paragraphs.get(i),
936            Err(_) => self.paragraphs.last(),
937        }
938    }
939
940    fn selection_geometry(
941        &self,
942        selection_range: Range<usize>,
943        mut callback: impl FnMut(PhysicalRect),
944    ) {
945        for paragraph in &self.paragraphs {
946            let selection_start = selection_range.start.max(paragraph.range.start);
947            let selection_end = selection_range.end.min(paragraph.range.end);
948
949            if selection_start < selection_end {
950                let local_start = selection_start - paragraph.range.start;
951                let local_end = selection_end - paragraph.range.start;
952
953                let selection = parley::editing::Selection::new(
954                    parley::editing::Cursor::from_byte_index(
955                        &paragraph.layout,
956                        local_start,
957                        Default::default(),
958                    ),
959                    parley::editing::Cursor::from_byte_index(
960                        &paragraph.layout,
961                        local_end,
962                        Default::default(),
963                    ),
964                );
965
966                selection.geometry_with(&paragraph.layout, |rect, _| {
967                    callback(PhysicalRect::new(
968                        PhysicalPoint::from_lengths(
969                            PhysicalLength::new(rect.x0 as _),
970                            PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
971                        ),
972                        PhysicalSize::new(rect.width() as _, rect.height() as _),
973                    ));
974                });
975            }
976        }
977    }
978
979    fn byte_offset_from_point(&self, pos: PhysicalPoint) -> usize {
980        let Some(paragraph) = self.paragraph_by_y(pos.y_length()) else {
981            return 0;
982        };
983        let cursor = parley::editing::Cursor::from_point(
984            &paragraph.layout,
985            pos.x,
986            (pos.y_length() - self.y_offset - paragraph.y).get(),
987        );
988        paragraph.range.start + cursor.index()
989    }
990
991    fn cursor_rect_for_byte_offset(
992        &self,
993        byte_offset: usize,
994        cursor_width: PhysicalLength,
995    ) -> PhysicalRect {
996        let Some(paragraph) = self.paragraph_by_byte_offset(byte_offset) else {
997            return PhysicalRect::new(PhysicalPoint::default(), PhysicalSize::new(1.0, 1.0));
998        };
999
1000        let local_offset = byte_offset - paragraph.range.start;
1001        let cursor = parley::editing::Cursor::from_byte_index(
1002            &paragraph.layout,
1003            local_offset,
1004            Default::default(),
1005        );
1006        let rect = cursor.geometry(&paragraph.layout, cursor_width.get());
1007
1008        PhysicalRect::new(
1009            PhysicalPoint::from_lengths(
1010                PhysicalLength::new(rect.x0 as _),
1011                PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
1012            ),
1013            PhysicalSize::new(rect.width() as _, rect.height() as _),
1014        )
1015    }
1016
1017    /// Returns an iterator over the run's glyphs, truncated if necessary to fit within the max width,
1018    /// plus an optional ellipsis glyph with its font and size to be drawn separately.
1019    /// Call this function only for the last line of the layout.
1020    fn glyphs_with_elision<'a>(
1021        &'a self,
1022        glyph_run: &'a parley::layout::GlyphRun<Brush>,
1023        // When set, place an ellipsis even if the run fits the width. Used when lines below were
1024        // dropped for the height, so the last visible line signals the vertical truncation.
1025        force_elision: bool,
1026    ) -> (
1027        impl Iterator<Item = parley::layout::Glyph> + Clone + 'a,
1028        Option<(parley::layout::Glyph, parley::FontData, PhysicalLength)>,
1029    ) {
1030        let ellipsis_advance =
1031            self.elision_info.as_ref().map(|info| info.ellipsis_glyph.advance).unwrap_or(0.0);
1032        let max_width = self
1033            .elision_info
1034            .as_ref()
1035            .map(|info| info.max_physical_width)
1036            .unwrap_or(PhysicalLength::new(f32::MAX));
1037
1038        let run_start = PhysicalLength::new(glyph_run.offset());
1039        let run_end = PhysicalLength::new(glyph_run.offset() + glyph_run.advance());
1040
1041        // Run starts after where the ellipsis would go - skip entirely
1042        let run_beyond_elision = run_start > max_width;
1043        // Run extends beyond max width (or the lines below it were dropped) and needs an ellipsis
1044        let needs_elision = !run_beyond_elision
1045            && (force_elision || run_end.get().floor() > max_width.get().ceil());
1046
1047        let truncated_glyphs = glyph_run.positioned_glyphs().take_while(move |glyph| {
1048            !run_beyond_elision
1049                && (!needs_elision
1050                    || PhysicalLength::new(glyph.x + glyph.advance + ellipsis_advance) <= max_width)
1051        });
1052
1053        let ellipsis = if needs_elision {
1054            self.elision_info.as_ref().map(|info| {
1055                let ellipsis_x = glyph_run
1056                    .positioned_glyphs()
1057                    .find(|glyph| {
1058                        PhysicalLength::new(glyph.x + glyph.advance + info.ellipsis_glyph.advance)
1059                            > info.max_physical_width
1060                    })
1061                    .map(|g| g.x)
1062                    // Nothing overflows horizontally (force_elision): put the ellipsis after the run.
1063                    .unwrap_or(run_end.get());
1064
1065                let mut ellipsis_glyph = info.ellipsis_glyph;
1066                ellipsis_glyph.x = ellipsis_x;
1067                // The ellipsis glyph comes from a standalone layout; place it on this run's
1068                // baseline so it lands on the right line (not just the first one).
1069                ellipsis_glyph.y = glyph_run.baseline();
1070
1071                let font_size = PhysicalLength::new(glyph_run.run().font_size());
1072                (ellipsis_glyph, info.font_for_ellipsis_glyph.clone(), font_size)
1073            })
1074        } else {
1075            None
1076        };
1077
1078        (truncated_glyphs, ellipsis)
1079    }
1080
1081    fn draw<R: GlyphRenderer>(
1082        &self,
1083        item_renderer: &mut R,
1084        default_fill_brush: <R as GlyphRenderer>::PlatformBrush,
1085        default_stroke_brush: Option<<R as GlyphRenderer>::PlatformBrush>,
1086        draw_glyphs: &mut dyn FnMut(
1087            &mut R,
1088            &parley::FontData,
1089            PhysicalLength,
1090            &[i16],               // normalized variation coords
1091            &fontique::Synthesis, // design-space variation settings
1092            <R as GlyphRenderer>::PlatformBrush,
1093            PhysicalLength, // y offset for paragraph
1094            &mut dyn Iterator<Item = parley::layout::Glyph>,
1095        ),
1096    ) {
1097        for paragraph in &self.paragraphs {
1098            paragraph.draw(
1099                self,
1100                item_renderer,
1101                &default_fill_brush,
1102                &default_stroke_brush,
1103                draw_glyphs,
1104            );
1105        }
1106    }
1107}
1108
1109pub fn draw_text(
1110    item_renderer: &mut impl GlyphRenderer,
1111    text: Pin<&dyn crate::item_rendering::RenderText>,
1112    item_rc: Option<&crate::item_tree::ItemRc>,
1113    size: LogicalSize,
1114    cache: Option<&TextLayoutCache>,
1115) {
1116    let max_width = size.width_length();
1117    let max_height = size.height_length();
1118
1119    if max_width.get() <= 0. || max_height.get() <= 0. {
1120        return;
1121    }
1122
1123    let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size)
1124    else {
1125        // Nothing to draw
1126        return;
1127    };
1128
1129    let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1130
1131    let (stroke_brush, stroke_width, stroke_style) = text.stroke();
1132    let platform_stroke_brush = if !stroke_brush.is_transparent() {
1133        let stroke_width = if stroke_width.get() != 0.0 {
1134            (stroke_width * scale_factor).get()
1135        } else {
1136            // Hairline stroke
1137            1.0
1138        };
1139        let stroke_width = match stroke_style {
1140            TextStrokeStyle::Outside => stroke_width * 2.0,
1141            TextStrokeStyle::Center => stroke_width,
1142        };
1143        item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size)
1144    } else {
1145        None
1146    };
1147
1148    // The layout_builder is still needed for the elision glyph in layout().
1149    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1150        item_rc.map(|item_rc| text.font_request(item_rc)),
1151        text.wrap(),
1152        platform_stroke_brush.is_some().then_some(stroke_style),
1153        scale_factor,
1154    );
1155
1156    let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1157
1158    let mut guard =
1159        get_or_create_text_paragraphs(cache, item_rc, text, scale_factor, &mut font_ctx);
1160
1161    let (horizontal_align, vertical_align) = text.alignment();
1162    let text_overflow = text.overflow();
1163
1164    let layout = layout(
1165        &layout_builder,
1166        &mut font_ctx,
1167        guard.paragraphs.take().unwrap_or_default(),
1168        scale_factor,
1169        LayoutOptions {
1170            horizontal_align,
1171            vertical_align,
1172            max_height: Some(max_height),
1173            max_width: Some(max_width),
1174            text_overflow: text.overflow(),
1175        },
1176    );
1177
1178    drop(font_ctx);
1179
1180    // When `overflow: elide` can't even fit the first line, the line is still drawn (rather than
1181    // dropped, which would render nothing) but its vertical overflow needs to be clipped like
1182    // `overflow: clip` would. Horizontal elision still applies, so a line that is both too tall
1183    // and too wide is clipped vertically and gets an ellipsis horizontally.
1184    let clip_overflowing_first_line =
1185        text_overflow == TextOverflow::Elide && layout.first_line_exceeds_height();
1186
1187    let render = if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
1188        item_renderer.save_state();
1189
1190        item_renderer.combine_clip(
1191            LogicalRect::new(LogicalPoint::default(), size),
1192            LogicalBorderRadius::zero(),
1193            LogicalLength::zero(),
1194        )
1195    } else {
1196        true
1197    };
1198
1199    if render {
1200        layout.draw(
1201            item_renderer,
1202            platform_fill_brush,
1203            platform_stroke_brush,
1204            &mut |item_renderer: &mut _,
1205                  font,
1206                  font_size,
1207                  normalized_coords,
1208                  synthesis,
1209                  brush,
1210                  y_offset,
1211                  glyphs_it| {
1212                item_renderer.draw_glyph_run(
1213                    font,
1214                    font_size,
1215                    normalized_coords,
1216                    synthesis,
1217                    brush,
1218                    y_offset,
1219                    glyphs_it,
1220                );
1221            },
1222        );
1223    }
1224
1225    if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
1226        item_renderer.restore_state();
1227    }
1228
1229    // Put paragraphs back into the cache guard for reuse.
1230    // break_all_lines replaces line data each time, so the state is ready for the next call.
1231    guard.paragraphs = Some(layout.paragraphs);
1232}
1233
1234#[cfg(feature = "std")]
1235pub fn link_under_cursor(
1236    font_context: &mut parley::FontContext,
1237    scale_factor: ScaleFactor,
1238    text: Pin<&dyn crate::item_rendering::RenderText>,
1239    item_rc: &crate::item_tree::ItemRc,
1240    size: LogicalSize,
1241    cursor: PhysicalPoint,
1242    cache: Option<&TextLayoutCache>,
1243) -> Option<std::string::String> {
1244    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1245        Some(text.font_request(item_rc)),
1246        text.wrap(),
1247        None,
1248        scale_factor,
1249    );
1250
1251    let mut guard =
1252        get_or_create_text_paragraphs(cache, Some(item_rc), text, scale_factor, font_context);
1253
1254    let (horizontal_align, vertical_align) = text.alignment();
1255
1256    let layout = layout(
1257        &layout_builder,
1258        font_context,
1259        guard.paragraphs.take().unwrap_or_default(),
1260        scale_factor,
1261        LayoutOptions {
1262            horizontal_align,
1263            vertical_align,
1264            max_height: Some(size.height_length()),
1265            max_width: Some(size.width_length()),
1266            text_overflow: text.overflow(),
1267        },
1268    );
1269
1270    let result = layout.paragraph_by_y(cursor.y_length()).and_then(|paragraph| {
1271        let paragraph_y: f64 = paragraph.y.cast::<f64>().get();
1272
1273        paragraph
1274            .links
1275            .iter()
1276            .find(|(range, _)| {
1277                let start = parley::editing::Cursor::from_byte_index(
1278                    &paragraph.layout,
1279                    range.start,
1280                    Default::default(),
1281                );
1282                let end = parley::editing::Cursor::from_byte_index(
1283                    &paragraph.layout,
1284                    range.end,
1285                    Default::default(),
1286                );
1287                let mut clicked = false;
1288                let link_range = parley::Selection::new(start, end);
1289                link_range.geometry_with(&paragraph.layout, |mut bounding_box, _line| {
1290                    bounding_box.y0 += paragraph_y;
1291                    bounding_box.y1 += paragraph_y;
1292                    clicked = bounding_box.union(parley::BoundingBox::new(
1293                        cursor.x.into(),
1294                        cursor.y.into(),
1295                        cursor.x.into(),
1296                        cursor.y.into(),
1297                    )) == bounding_box;
1298                });
1299                clicked
1300            })
1301            .map(|(_, link)| link.clone())
1302    });
1303
1304    // Put paragraphs back into the cache guard for reuse.
1305    guard.paragraphs = Some(layout.paragraphs);
1306
1307    result
1308}
1309
1310pub fn draw_text_input(
1311    item_renderer: &mut impl GlyphRenderer,
1312    text_input: Pin<&crate::items::TextInput>,
1313    item_rc: &crate::item_tree::ItemRc,
1314    size: LogicalSize,
1315    password_character: Option<fn() -> char>,
1316) {
1317    let width = size.width_length();
1318    let height = size.height_length();
1319    if width.get() <= 0. || height.get() <= 0. {
1320        return;
1321    }
1322
1323    let visual_representation = text_input.visual_representation(password_character);
1324
1325    let Some(platform_fill_brush) =
1326        item_renderer.platform_text_fill_brush(visual_representation.text_color, size)
1327    else {
1328        return;
1329    };
1330
1331    let selection_range = if !visual_representation.preedit_range.is_empty() {
1332        visual_representation.preedit_range.start..visual_representation.preedit_range.end
1333    } else {
1334        visual_representation.selection_range.start..visual_representation.selection_range.end
1335    };
1336
1337    let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1338
1339    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1340        Some(text_input.font_request(item_rc)),
1341        text_input.wrap(),
1342        None,
1343        scale_factor,
1344    );
1345
1346    let text = visual_representation.text.clone();
1347
1348    // When a piece of text is first selected, it gets an empty range like `Some(1..1)`.
1349    // If the text starts with a multi-byte character then this selection will be within
1350    // that character and parley will panic. We just filter out empty selection ranges.
1351    let selection_and_color = if !selection_range.is_empty() {
1352        Some((selection_range.clone(), text_input.selection_foreground_color()))
1353    } else {
1354        None
1355    };
1356
1357    let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1358
1359    let paragraphs_without_linebreaks = create_text_paragraphs(
1360        &layout_builder,
1361        &mut font_ctx,
1362        PlainOrStyledText::Plain(text),
1363        selection_and_color,
1364        Color::default(),
1365    );
1366
1367    let layout = layout(
1368        &layout_builder,
1369        &mut font_ctx,
1370        paragraphs_without_linebreaks,
1371        scale_factor,
1372        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1373    );
1374
1375    drop(font_ctx);
1376
1377    layout.selection_geometry(selection_range, |selection_rect| {
1378        item_renderer
1379            .fill_rectangle_with_color(selection_rect, text_input.selection_background_color());
1380    });
1381
1382    item_renderer.save_state();
1383
1384    let render = item_renderer.combine_clip(
1385        LogicalRect::new(LogicalPoint::default(), size),
1386        LogicalBorderRadius::zero(),
1387        LogicalLength::zero(),
1388    );
1389
1390    if render {
1391        layout.draw(
1392            item_renderer,
1393            platform_fill_brush,
1394            None,
1395            &mut |item_renderer: &mut _,
1396                  font,
1397                  font_size,
1398                  normalized_coords,
1399                  synthesis,
1400                  brush,
1401                  y_offset,
1402                  glyphs_it| {
1403                item_renderer.draw_glyph_run(
1404                    font,
1405                    font_size,
1406                    normalized_coords,
1407                    synthesis,
1408                    brush,
1409                    y_offset,
1410                    glyphs_it,
1411                );
1412            },
1413        );
1414
1415        if let Some(cursor_pos) = visual_representation.cursor_position {
1416            let cursor_rect = layout.cursor_rect_for_byte_offset(
1417                cursor_pos,
1418                text_input.text_cursor_width() * scale_factor,
1419            );
1420            item_renderer
1421                .fill_rectangle_with_color(cursor_rect, visual_representation.cursor_color);
1422        }
1423    }
1424
1425    item_renderer.restore_state();
1426}
1427
1428pub fn text_size(
1429    renderer: &dyn RendererSealed,
1430    text_item: Pin<&dyn crate::item_rendering::RenderString>,
1431    item_rc: &crate::item_tree::ItemRc,
1432    max_width: Option<LogicalLength>,
1433    text_wrap: TextWrap,
1434    _cache: Option<&TextLayoutCache>,
1435) -> Option<LogicalSize> {
1436    let scale_factor = renderer.scale_factor()?;
1437
1438    // Evaluate properties before borrowing font_context: both font_request()
1439    // and text() can trigger property bindings that re-enter text_size for
1440    // other elements, which would panic on a second borrow_mut().
1441    let font_request = text_item.font_request(item_rc);
1442    let text = text_item.text();
1443
1444    let ctx = renderer.slint_context()?;
1445    let mut font_ctx = ctx.font_context().borrow_mut();
1446
1447    let layout_builder =
1448        LayoutWithoutLineBreaksBuilder::new(Some(font_request), text_wrap, None, scale_factor);
1449
1450    let paragraphs_without_linebreaks =
1451        create_text_paragraphs(&layout_builder, &mut font_ctx, text, None, Color::default());
1452
1453    let layout = layout(
1454        &layout_builder,
1455        &mut font_ctx,
1456        paragraphs_without_linebreaks,
1457        scale_factor,
1458        LayoutOptions {
1459            max_width,
1460            max_height: None,
1461            horizontal_align: TextHorizontalAlignment::Left,
1462            vertical_align: TextVerticalAlignment::Top,
1463            text_overflow: TextOverflow::Clip,
1464        },
1465    );
1466    Some(PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor)
1467}
1468
1469pub fn char_size(
1470    font_ctx: &mut parley::FontContext,
1471    text_item: Pin<&dyn crate::item_rendering::HasFont>,
1472    item_rc: &crate::item_tree::ItemRc,
1473    ch: char,
1474) -> Option<LogicalSize> {
1475    let font_request = text_item.font_request(item_rc);
1476    let font = font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
1477
1478    let char_map = font.charmap()?;
1479
1480    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1481
1482    let glyph_index = char_map.map(ch)?;
1483
1484    let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE);
1485
1486    let location = face.axes().location(font.synthesis.variation_settings());
1487
1488    let glyph_metrics = skrifa::metrics::GlyphMetrics::new(
1489        &face,
1490        skrifa::instance::Size::new(pixel_size.get()),
1491        &location,
1492    );
1493
1494    let advance_width = LogicalLength::new(glyph_metrics.advance_width(glyph_index.into())?);
1495
1496    let font_metrics = skrifa::metrics::Metrics::new(
1497        &face,
1498        skrifa::instance::Size::new(pixel_size.get()),
1499        &location,
1500    );
1501
1502    Some(LogicalSize::from_lengths(
1503        advance_width,
1504        LogicalLength::new(font_metrics.ascent - font_metrics.descent),
1505    ))
1506}
1507
1508pub fn font_metrics(
1509    font_ctx: &mut parley::FontContext,
1510    font_request: FontRequest,
1511) -> crate::items::FontMetrics {
1512    let logical_pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE).get();
1513
1514    let Some(font) =
1515        font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)
1516    else {
1517        return crate::items::FontMetrics::default();
1518    };
1519
1520    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1521    let location = face.axes().location(font.synthesis.variation_settings());
1522    let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
1523
1524    let units_per_em = metrics.units_per_em as f32;
1525
1526    crate::items::FontMetrics {
1527        ascent: metrics.ascent * logical_pixel_size / units_per_em,
1528        descent: metrics.descent * logical_pixel_size / units_per_em,
1529        x_height: metrics.x_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1530        cap_height: metrics.cap_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1531    }
1532}
1533
1534pub fn text_input_byte_offset_for_position(
1535    renderer: &dyn RendererSealed,
1536    text_input: Pin<&crate::items::TextInput>,
1537    item_rc: &crate::item_tree::ItemRc,
1538    pos: LogicalPoint,
1539) -> usize {
1540    let Some(scale_factor) = renderer.scale_factor() else {
1541        return 0;
1542    };
1543    let pos: PhysicalPoint = pos * scale_factor;
1544
1545    let width = text_input.width();
1546    let height = text_input.height();
1547    if width.get() <= 0. || height.get() <= 0. || pos.y < 0. {
1548        return 0;
1549    }
1550
1551    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1552        Some(text_input.font_request(item_rc)),
1553        text_input.wrap(),
1554        None,
1555        scale_factor,
1556    );
1557    let visual_representation = text_input.visual_representation(None);
1558
1559    let Some(ctx) = renderer.slint_context() else {
1560        return 0;
1561    };
1562    let mut font_ctx = ctx.font_context().borrow_mut();
1563
1564    let paragraphs_without_linebreaks = create_text_paragraphs(
1565        &layout_builder,
1566        &mut font_ctx,
1567        PlainOrStyledText::Plain(visual_representation.text.clone()),
1568        None,
1569        Color::default(),
1570    );
1571
1572    let layout = layout(
1573        &layout_builder,
1574        &mut font_ctx,
1575        paragraphs_without_linebreaks,
1576        scale_factor,
1577        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1578    );
1579    let byte_offset = layout.byte_offset_from_point(pos);
1580    visual_representation.map_byte_offset_from_visual_text_to_actual_text(byte_offset)
1581}
1582
1583pub fn text_input_cursor_rect_for_byte_offset(
1584    renderer: &dyn RendererSealed,
1585    text_input: Pin<&crate::items::TextInput>,
1586    item_rc: &crate::item_tree::ItemRc,
1587    byte_offset: usize,
1588) -> LogicalRect {
1589    let Some(scale_factor) = renderer.scale_factor() else {
1590        return LogicalRect::default();
1591    };
1592
1593    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1594        Some(text_input.font_request(item_rc)),
1595        text_input.wrap(),
1596        None,
1597        scale_factor,
1598    );
1599
1600    let width = text_input.width();
1601    let height = text_input.height();
1602    if width.get() <= 0. || height.get() <= 0. {
1603        return LogicalRect::new(
1604            LogicalPoint::default(),
1605            LogicalSize::from_lengths(LogicalLength::new(1.0), layout_builder.pixel_size),
1606        );
1607    }
1608
1609    let visual_representation = text_input.visual_representation(None);
1610    let cursor_width = text_input.text_cursor_width() * scale_factor;
1611
1612    let Some(ctx) = renderer.slint_context() else {
1613        return LogicalRect::default();
1614    };
1615
1616    let mut font_ctx = ctx.font_context().borrow_mut();
1617
1618    let byte_offset = visual_representation.map_byte_offset_from_actual_to_visual_text(byte_offset);
1619
1620    let paragraphs_without_linebreaks = create_text_paragraphs(
1621        &layout_builder,
1622        &mut font_ctx,
1623        PlainOrStyledText::Plain(visual_representation.text),
1624        None,
1625        Color::default(),
1626    );
1627
1628    let layout = layout(
1629        &layout_builder,
1630        &mut font_ctx,
1631        paragraphs_without_linebreaks,
1632        scale_factor,
1633        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1634    );
1635    let cursor_rect = layout.cursor_rect_for_byte_offset(byte_offset, cursor_width);
1636    cursor_rect / scale_factor
1637}