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
4pub use parley;
5pub use parley::fontique;
6
7use alloc::vec::Vec;
8use core::ops::Range;
9use core::pin::Pin;
10use euclid::num::Zero;
11use skrifa::MetadataProvider as _;
12use std::cell::RefCell;
13
14use crate::{
15    Color, SharedString,
16    graphics::FontRequest,
17    item_rendering::PlainOrStyledText,
18    items::TextStrokeStyle,
19    lengths::{
20        LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx,
21        PointLengths, ScaleFactor, SizeLengths,
22    },
23    renderer::RendererSealed,
24    textlayout::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap},
25};
26
27type InnerTextLayoutCache = crate::item_rendering::ItemCache<Vec<TextParagraph>>;
28
29/// Cache for shaped text paragraphs (before line breaking), keyed by ItemRc.
30pub struct TextLayoutCache {
31    inner: InnerTextLayoutCache,
32    #[cfg(feature = "testing")]
33    cache_miss_count: std::cell::Cell<u64>,
34}
35
36#[allow(clippy::derivable_impls)] // clippy doesn't see the feature = "testing" code
37impl Default for TextLayoutCache {
38    fn default() -> Self {
39        Self {
40            inner: Default::default(),
41            #[cfg(feature = "testing")]
42            cache_miss_count: std::cell::Cell::new(0),
43        }
44    }
45}
46
47impl TextLayoutCache {
48    pub fn clear_cache_if_scale_factor_changed(&self, window: &crate::api::Window) {
49        self.inner.clear_cache_if_scale_factor_changed(window);
50    }
51    pub fn component_destroyed(&self, component: crate::item_tree::ItemTreeRef) {
52        self.inner.component_destroyed(component);
53    }
54    pub fn clear_all(&self) {
55        self.inner.clear_all();
56    }
57}
58
59#[cfg(feature = "testing")]
60impl TextLayoutCache {
61    pub fn cache_miss_count(&self) -> u64 {
62        self.cache_miss_count.get()
63    }
64    pub fn reset_cache_miss_count(&self) {
65        self.cache_miss_count.set(0);
66    }
67}
68
69pub type PhysicalLength = euclid::Length<f32, PhysicalPx>;
70pub type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
71type PhysicalSize = euclid::Size2D<f32, PhysicalPx>;
72type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>;
73
74use i_slint_common::sharedfontique;
75
76/// Trait used for drawing text and text input elements with parley, where parley does the
77/// shaping and positioning, and the renderer is responsible for drawing just the glyphs.
78pub trait GlyphRenderer: crate::item_rendering::ItemRenderer {
79    /// A renderer-specific type for a brush used for fill and stroke of glyphs.
80    type PlatformBrush: Clone;
81
82    /// Returns the brush to be used for filling text.
83    fn platform_text_fill_brush(
84        &mut self,
85        brush: crate::Brush,
86        size: LogicalSize,
87    ) -> Option<Self::PlatformBrush>;
88
89    /// Returns a brush that's a solid fill of the specified color.
90    fn platform_brush_for_color(&mut self, color: &Color) -> Option<Self::PlatformBrush>;
91
92    /// Returns the brush to be used for stroking text.
93    fn platform_text_stroke_brush(
94        &mut self,
95        brush: crate::Brush,
96        physical_stroke_width: f32,
97        size: LogicalSize,
98    ) -> Option<Self::PlatformBrush>;
99
100    /// Draws the glyphs provided by glyphs_it with the specified font, font_size, and brush at the
101    /// given y offset. The `normalized_coords` are F2Dot14 values in fvar axis order for variable
102    /// font rendering. The `synthesis` contains design-space variation settings and faux
103    /// bold/italic hints from fontique.
104    fn draw_glyph_run(
105        &mut self,
106        font: &parley::FontData,
107        font_size: PhysicalLength,
108        normalized_coords: &[i16],
109        synthesis: &fontique::Synthesis,
110        brush: Self::PlatformBrush,
111        y_offset: PhysicalLength,
112        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
113    );
114
115    fn fill_rectange_with_color(&mut self, physical_rect: PhysicalRect, color: Color) {
116        if let Some(platform_brush) = self.platform_brush_for_color(&color) {
117            self.fill_rectangle(physical_rect, platform_brush);
118        }
119    }
120
121    /// Fills the given rectangle with the specified color. This is used for drawing selection
122    /// rectangles as well as the text cursor.
123    fn fill_rectangle(&mut self, physical_rect: PhysicalRect, brush: Self::PlatformBrush);
124}
125
126pub const DEFAULT_FONT_SIZE: LogicalLength = LogicalLength::new(12.);
127
128std::thread_local! {
129    static LAYOUT_CONTEXT: RefCell<parley::LayoutContext<Brush>> = Default::default();
130}
131
132#[derive(Debug, Default, PartialEq, Clone, Copy)]
133struct Brush {
134    /// When set, this overrides the fill/stroke to use this color.
135    override_fill_color: Option<Color>,
136    stroke: Option<TextStrokeStyle>,
137    link_color: Option<Color>,
138}
139
140struct LayoutOptions {
141    max_width: Option<LogicalLength>,
142    max_height: Option<LogicalLength>,
143    horizontal_align: TextHorizontalAlignment,
144    vertical_align: TextVerticalAlignment,
145    text_overflow: TextOverflow,
146}
147
148impl LayoutOptions {
149    fn new_from_textinput(
150        text_input: Pin<&crate::items::TextInput>,
151        max_width: Option<LogicalLength>,
152        max_height: Option<LogicalLength>,
153    ) -> Self {
154        Self {
155            max_width,
156            max_height,
157            horizontal_align: text_input.horizontal_alignment(),
158            vertical_align: text_input.vertical_alignment(),
159            text_overflow: TextOverflow::Clip,
160        }
161    }
162}
163
164struct LayoutWithoutLineBreaksBuilder {
165    font_request: Option<FontRequest>,
166    text_wrap: TextWrap,
167    stroke: Option<TextStrokeStyle>,
168    scale_factor: ScaleFactor,
169    pixel_size: LogicalLength,
170}
171
172impl LayoutWithoutLineBreaksBuilder {
173    fn new(
174        font_request: Option<FontRequest>,
175        text_wrap: TextWrap,
176        stroke: Option<TextStrokeStyle>,
177        scale_factor: ScaleFactor,
178    ) -> Self {
179        let pixel_size = font_request
180            .as_ref()
181            .and_then(|font_request| font_request.pixel_size)
182            .unwrap_or(DEFAULT_FONT_SIZE);
183
184        Self { font_request, text_wrap, stroke, scale_factor, pixel_size }
185    }
186
187    fn ranged_builder<'a>(
188        &self,
189        layout_ctx: &'a mut parley::LayoutContext<Brush>,
190        font_ctx: &'a mut parley::FontContext,
191        text: &'a str,
192    ) -> parley::RangedBuilder<'a, Brush> {
193        let mut builder = layout_ctx.ranged_builder(font_ctx, text, self.scale_factor.get(), false);
194
195        if let Some(ref font_request) = self.font_request {
196            let mut fallback_family_iter = sharedfontique::FALLBACK_FAMILIES
197                .into_iter()
198                .map(parley::style::FontFamilyName::Generic);
199
200            let font_families: &[parley::style::FontFamilyName] = if let Some(family) =
201                &font_request.family
202            {
203                let mut iter =
204                    core::iter::once(parley::style::FontFamilyName::named(family.as_str()))
205                        .chain(fallback_family_iter);
206                &core::array::from_fn::<
207                    _,
208                    { sharedfontique::FALLBACK_FAMILIES.as_slice().len() + 1 },
209                    _,
210                >(|_| iter.next().unwrap())
211            } else {
212                &core::array::from_fn::<_, { sharedfontique::FALLBACK_FAMILIES.as_slice().len() }, _>(
213                    |_| fallback_family_iter.next().unwrap(),
214                )
215            };
216
217            builder.push_default(parley::style::FontFamily::List(std::borrow::Cow::Borrowed(
218                font_families,
219            )));
220
221            if let Some(weight) = font_request.weight {
222                builder.push_default(parley::StyleProperty::FontWeight(
223                    parley::style::FontWeight::new(weight as f32),
224                ));
225            }
226            if let Some(letter_spacing) = font_request.letter_spacing {
227                builder.push_default(parley::StyleProperty::LetterSpacing(letter_spacing.get()));
228            }
229            builder.push_default(parley::StyleProperty::FontStyle(if font_request.italic {
230                parley::style::FontStyle::Italic
231            } else {
232                parley::style::FontStyle::Normal
233            }));
234        }
235        builder.push_default(parley::StyleProperty::FontSize(self.pixel_size.get()));
236        builder.push_default(parley::StyleProperty::WordBreak(match self.text_wrap {
237            TextWrap::NoWrap => parley::style::WordBreak::KeepAll,
238            TextWrap::WordWrap => parley::style::WordBreak::Normal,
239            TextWrap::CharWrap => parley::style::WordBreak::BreakAll,
240        }));
241        builder.push_default(parley::StyleProperty::OverflowWrap(match self.text_wrap {
242            TextWrap::NoWrap => parley::style::OverflowWrap::Normal,
243            TextWrap::WordWrap | TextWrap::CharWrap => parley::style::OverflowWrap::Anywhere,
244        }));
245
246        builder.push_default(parley::StyleProperty::Brush(Brush {
247            override_fill_color: None,
248            stroke: self.stroke,
249            link_color: None,
250        }));
251
252        builder
253    }
254
255    fn build(
256        &self,
257        font_context: &mut parley::FontContext,
258        text: &str,
259        selection: Option<(Range<usize>, Color)>,
260        formatting: impl IntoIterator<Item = i_slint_common::styled_text::FormattedSpan>,
261        link_color: Option<Color>,
262    ) -> parley::Layout<Brush> {
263        use i_slint_common::styled_text::Style;
264
265        LAYOUT_CONTEXT.with_borrow_mut(|layout_ctx| {
266            let mut builder = self.ranged_builder(layout_ctx, font_context, text);
267
268            if let Some((selection_range, selection_color)) = selection {
269                {
270                    builder.push(
271                        parley::StyleProperty::Brush(Brush {
272                            override_fill_color: Some(selection_color),
273                            stroke: self.stroke,
274                            link_color: None,
275                        }),
276                        selection_range,
277                    );
278                }
279            }
280
281            // filter empty ranges otherwise parley will panic on assert
282            for span in formatting.into_iter().filter(|s| !s.range.is_empty()) {
283                match span.style {
284                    Style::Emphasis => {
285                        builder.push(
286                            parley::StyleProperty::FontStyle(parley::style::FontStyle::Italic),
287                            span.range,
288                        );
289                    }
290                    Style::Strikethrough => {
291                        builder.push(parley::StyleProperty::Strikethrough(true), span.range);
292                    }
293                    Style::Strong => {
294                        builder.push(
295                            parley::StyleProperty::FontWeight(parley::style::FontWeight::BOLD),
296                            span.range,
297                        );
298                    }
299                    Style::Code => {
300                        builder.push(
301                            parley::StyleProperty::FontFamily(parley::style::FontFamily::Single(
302                                parley::style::FontFamilyName::Generic(
303                                    parley::style::GenericFamily::Monospace,
304                                ),
305                            )),
306                            span.range,
307                        );
308                    }
309                    Style::Underline => {
310                        builder.push(parley::StyleProperty::Underline(true), span.range);
311                    }
312                    Style::Link => {
313                        builder.push(parley::StyleProperty::Underline(true), span.range.clone());
314                        builder.push(
315                            parley::StyleProperty::Brush(Brush {
316                                override_fill_color: None,
317                                stroke: self.stroke,
318                                link_color,
319                            }),
320                            span.range,
321                        );
322                    }
323                    Style::Color(color) => {
324                        builder.push(
325                            parley::StyleProperty::Brush(Brush {
326                                override_fill_color: Some(crate::Color::from_argb_encoded(color)),
327                                stroke: self.stroke,
328                                link_color: None,
329                            }),
330                            span.range,
331                        );
332                    }
333                }
334            }
335
336            builder.build(text)
337        })
338    }
339}
340
341fn create_text_paragraphs(
342    layout_builder: &LayoutWithoutLineBreaksBuilder,
343    font_context: &mut parley::FontContext,
344    text: PlainOrStyledText,
345    selection: Option<(Range<usize>, Color)>,
346    link_color: Color,
347) -> Vec<TextParagraph> {
348    let paragraph_from_text =
349        |font_context: &mut parley::FontContext,
350         text: &str,
351         range: std::ops::Range<usize>,
352         formatting: Vec<i_slint_common::styled_text::FormattedSpan>,
353         links: Vec<(std::ops::Range<usize>, std::string::String)>| {
354            let selection = selection.clone().and_then(|(selection, selection_color)| {
355                let sel_start = selection.start.max(range.start);
356                let sel_end = selection.end.min(range.end);
357
358                if sel_start < sel_end {
359                    let local_selection = (sel_start - range.start)..(sel_end - range.start);
360                    Some((local_selection, selection_color))
361                } else {
362                    None
363                }
364            });
365
366            let layout =
367                layout_builder.build(font_context, text, selection, formatting, Some(link_color));
368
369            TextParagraph { range, y: PhysicalLength::default(), layout, links }
370        };
371
372    let mut paragraphs = Vec::with_capacity(1);
373
374    match text {
375        PlainOrStyledText::Plain(ref text) => {
376            let paragraph_ranges = core::iter::from_fn({
377                let mut start = 0;
378                let mut char_it = text.char_indices().peekable();
379                let mut eot = false;
380                move || {
381                    for (idx, ch) in char_it.by_ref() {
382                        if ch == '\n' {
383                            let next_range = start..idx;
384                            start = idx + ch.len_utf8();
385                            return Some(next_range);
386                        }
387                    }
388
389                    if eot {
390                        return None;
391                    }
392                    eot = true;
393                    Some(start..text.len())
394                }
395            });
396
397            for range in paragraph_ranges {
398                paragraphs.push(paragraph_from_text(
399                    font_context,
400                    &text[range.clone()],
401                    range,
402                    Default::default(),
403                    Default::default(),
404                ));
405            }
406        }
407        PlainOrStyledText::Styled(rich_text) => {
408            for paragraph in rich_text.paragraphs {
409                paragraphs.push(paragraph_from_text(
410                    font_context,
411                    &paragraph.text,
412                    0..0,
413                    paragraph.formatting,
414                    paragraph.links,
415                ));
416            }
417        }
418    };
419
420    paragraphs
421}
422
423/// `text_wrap` is passed separately from the shaped paragraphs because
424/// `text_layout_info()` calls `text_size()` with `NoWrap` for horizontal sizing.
425/// Note: parley currently uses `WordBreak` while shaping via `analyze_text()`,
426/// so shaped paragraphs aren't identical across wrap modes. This is why `text_size()`
427/// doesn't use the `TextLayoutCache` — it would be incorrect to share cached paragraphs
428/// shaped with one wrap mode and reuse them with another.
429fn layout(
430    layout_builder: &LayoutWithoutLineBreaksBuilder,
431    font_context: &mut parley::FontContext,
432    mut paragraphs: Vec<TextParagraph>,
433    scale_factor: ScaleFactor,
434    text_wrap: TextWrap,
435    options: LayoutOptions,
436) -> Layout {
437    let max_physical_width = options.max_width.map(|max_width| max_width * scale_factor);
438    let max_physical_height = options.max_height.map(|max_height| max_height * scale_factor);
439
440    // Returned None if failed to get the elipsis glyph for some rare reason.
441    let get_elipsis_glyph = |font_context: &mut parley::FontContext| {
442        let mut layout = layout_builder.build(font_context, "…", None, None, None);
443        layout.break_all_lines(None);
444        let line = layout.lines().next()?;
445        let item = line.items().next()?;
446        let run = match item {
447            parley::layout::PositionedLayoutItem::GlyphRun(run) => Some(run),
448            _ => return None,
449        }?;
450        let glyph = run.positioned_glyphs().next()?;
451        Some((glyph, run.run().font().clone()))
452    };
453
454    let elision_info =
455        if let (TextOverflow::Elide, Some(max_physical_width)) =
456            (options.text_overflow, max_physical_width)
457        {
458            get_elipsis_glyph(font_context).map(|(elipsis_glyph, font_for_elipsis_glyph)| {
459                ElisionInfo { elipsis_glyph, font_for_elipsis_glyph, max_physical_width }
460            })
461        } else {
462            None
463        };
464
465    let mut para_y = 0.0;
466    for para in paragraphs.iter_mut() {
467        para.layout.break_all_lines(
468            max_physical_width.filter(|_| text_wrap != TextWrap::NoWrap).map(|width| width.get()),
469        );
470        para.layout.align(
471            max_physical_width.map(|width| width.get()),
472            match options.horizontal_align {
473                TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
474                    parley::Alignment::Left
475                }
476                TextHorizontalAlignment::Center => parley::Alignment::Center,
477                TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
478                    parley::Alignment::Right
479                }
480            },
481            parley::AlignmentOptions::default(),
482        );
483
484        para.y = PhysicalLength::new(para_y);
485        para_y += para.layout.height();
486    }
487
488    let max_width = paragraphs
489        .iter()
490        .map(|p| {
491            // The max width is used for the elipsis computation when eliding text. We *want* to exclude whitespace
492            // for that, but we can't at the glyph run level, so the glyph runs always *do* include whitespace glyphs,
493            // and as such we must also accept the full width here including trailing whitespace, otherwise text with
494            // trailing whitespace will assigned a smaller width for rendering and thus the elipsis will be placed.
495            PhysicalLength::new(p.layout.full_width())
496        })
497        .fold(PhysicalLength::zero(), PhysicalLength::max);
498    let height = paragraphs
499        .last()
500        .map_or(PhysicalLength::zero(), |p| p.y + PhysicalLength::new(p.layout.height()));
501
502    let y_offset = match (max_physical_height, options.vertical_align) {
503        (Some(max_height), TextVerticalAlignment::Center) => (max_height - height) / 2.0,
504        (Some(max_height), TextVerticalAlignment::Bottom) => max_height - height,
505        (None, _) | (Some(_), TextVerticalAlignment::Top) => PhysicalLength::new(0.0),
506    };
507
508    Layout { paragraphs, y_offset, elision_info, max_width, height, max_physical_height }
509}
510
511/// RAII guard: takes Vec out of the cache on creation, puts it back on drop.
512struct CachedParagraphsGuard<'a> {
513    paragraphs: Option<Vec<TextParagraph>>,
514    container: Option<std::cell::RefMut<'a, Vec<TextParagraph>>>,
515}
516
517impl Drop for CachedParagraphsGuard<'_> {
518    fn drop(&mut self) {
519        if let (Some(paragraphs), Some(container)) = (self.paragraphs.take(), &mut self.container) {
520            **container = paragraphs;
521        }
522    }
523}
524
525fn shape_paragraphs(
526    text: Pin<&dyn crate::item_rendering::RenderText>,
527    item_rc: Option<&crate::item_tree::ItemRc>,
528    scale_factor: ScaleFactor,
529    font_context: &mut parley::FontContext,
530) -> Vec<TextParagraph> {
531    let (stroke_brush, _, stroke_style) = text.stroke();
532    let has_stroke = !stroke_brush.is_transparent();
533    let builder = LayoutWithoutLineBreaksBuilder::new(
534        item_rc.map(|irc| text.font_request(irc)),
535        text.wrap(),
536        has_stroke.then_some(stroke_style),
537        scale_factor,
538    );
539    create_text_paragraphs(&builder, font_context, text.text(), None, text.link_color())
540}
541
542fn get_or_create_text_paragraphs<'a>(
543    cache: Option<&'a TextLayoutCache>,
544    item_rc: Option<&crate::item_tree::ItemRc>,
545    text: Pin<&dyn crate::item_rendering::RenderText>,
546    scale_factor: ScaleFactor,
547    font_context: &mut parley::FontContext,
548) -> CachedParagraphsGuard<'a> {
549    if let (Some(cache), Some(item_rc)) = (cache, item_rc) {
550        let mut entry = cache.inner.get_or_update_cache_entry_ref(item_rc, || {
551            #[cfg(feature = "testing")]
552            cache.cache_miss_count.set(cache.cache_miss_count.get() + 1);
553            shape_paragraphs(text, Some(item_rc), scale_factor, font_context)
554        });
555        let paragraphs = std::mem::take(&mut *entry);
556        CachedParagraphsGuard { paragraphs: Some(paragraphs), container: Some(entry) }
557    } else {
558        CachedParagraphsGuard {
559            paragraphs: Some(shape_paragraphs(text, item_rc, scale_factor, font_context)),
560            container: None,
561        }
562    }
563}
564
565struct ElisionInfo {
566    elipsis_glyph: parley::layout::Glyph,
567    font_for_elipsis_glyph: parley::FontData,
568    max_physical_width: PhysicalLength,
569}
570
571struct TextParagraph {
572    range: Range<usize>,
573    y: PhysicalLength,
574    layout: parley::Layout<Brush>,
575    links: std::vec::Vec<(Range<usize>, std::string::String)>,
576}
577
578impl TextParagraph {
579    fn draw<R: GlyphRenderer>(
580        &self,
581        layout: &Layout,
582        item_renderer: &mut R,
583        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
584        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
585        draw_glyphs: &mut dyn FnMut(
586            &mut R,
587            &parley::FontData,
588            PhysicalLength,
589            &[i16],               // normalized variation coords
590            &fontique::Synthesis, // design-space variation settings
591            <R as GlyphRenderer>::PlatformBrush,
592            PhysicalLength, // y offset for paragraph
593            &mut dyn Iterator<Item = parley::layout::Glyph>,
594        ),
595    ) {
596        let para_y = layout.y_offset + self.y;
597
598        let mut lines = self
599            .layout
600            .lines()
601            .take_while(|line| {
602                let metrics = line.metrics();
603                match layout.max_physical_height {
604                    // If overflow: clip is set, we apply a hard pixel clip, but with overflow: elide,
605                    // we want to place an elipsis on the last line and not draw any lines beyond the
606                    // given max height.
607                    Some(max_physical_height) if layout.elision_info.is_some() => {
608                        max_physical_height.get().ceil() >= metrics.max_coord
609                    }
610                    _ => true,
611                }
612            })
613            .peekable();
614
615        while let Some(line) = lines.next() {
616            let last_line = lines.peek().is_none();
617            for item in line.items() {
618                match item {
619                    parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
620                        let elipsis = if last_line {
621                            let (truncated_glyphs, elipsis) =
622                                layout.glyphs_with_elision(&glyph_run);
623
624                            Self::draw_glyph_run(
625                                &glyph_run,
626                                item_renderer,
627                                default_fill_brush,
628                                default_stroke_brush,
629                                para_y,
630                                &mut truncated_glyphs.into_iter(),
631                                draw_glyphs,
632                            );
633                            elipsis
634                        } else {
635                            Self::draw_glyph_run(
636                                &glyph_run,
637                                item_renderer,
638                                default_fill_brush,
639                                default_stroke_brush,
640                                para_y,
641                                &mut glyph_run.positioned_glyphs(),
642                                draw_glyphs,
643                            );
644                            None
645                        };
646
647                        if let Some((elipsis_glyph, elipsis_font, font_size)) = elipsis {
648                            let run = glyph_run.run();
649                            draw_glyphs(
650                                item_renderer,
651                                &elipsis_font,
652                                font_size,
653                                run.normalized_coords(),
654                                &run.synthesis(),
655                                default_fill_brush.clone(),
656                                para_y,
657                                &mut core::iter::once(elipsis_glyph),
658                            );
659                        }
660                    }
661                    parley::PositionedLayoutItem::InlineBox(_inline_box) => {}
662                };
663            }
664        }
665    }
666
667    fn draw_glyph_run<R: GlyphRenderer>(
668        glyph_run: &parley::layout::GlyphRun<Brush>,
669        item_renderer: &mut R,
670        default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
671        default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
672        para_y: PhysicalLength,
673        glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
674        draw_glyphs: &mut dyn FnMut(
675            &mut R,
676            &parley::FontData,
677            PhysicalLength,
678            &[i16],               // normalized variation coords
679            &fontique::Synthesis, // design-space variation settings
680            <R as GlyphRenderer>::PlatformBrush,
681            PhysicalLength,
682            &mut dyn Iterator<Item = parley::layout::Glyph>,
683        ),
684    ) {
685        let run = glyph_run.run();
686        let normalized_coords = run.normalized_coords();
687        let synthesis = run.synthesis();
688        let brush = &glyph_run.style().brush;
689
690        let (fill_brush, stroke_style) = match (brush.override_fill_color, brush.link_color) {
691            (Some(color), _) => {
692                let Some(selection_brush) = item_renderer.platform_brush_for_color(&color) else {
693                    return;
694                };
695                (selection_brush.clone(), &None)
696            }
697            (None, Some(color)) => {
698                let Some(link_brush) = item_renderer.platform_brush_for_color(&color) else {
699                    return;
700                };
701                (link_brush.clone(), &None)
702            }
703            (None, None) => (default_fill_brush.clone(), &brush.stroke),
704        };
705
706        match stroke_style {
707            Some(TextStrokeStyle::Outside) => {
708                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
709
710                if let Some(stroke_brush) = default_stroke_brush.clone() {
711                    draw_glyphs(
712                        item_renderer,
713                        run.font(),
714                        PhysicalLength::new(run.font_size()),
715                        normalized_coords,
716                        &synthesis,
717                        stroke_brush,
718                        para_y,
719                        &mut glyphs.iter().cloned(),
720                    );
721                }
722
723                draw_glyphs(
724                    item_renderer,
725                    run.font(),
726                    PhysicalLength::new(run.font_size()),
727                    normalized_coords,
728                    &synthesis,
729                    fill_brush.clone(),
730                    para_y,
731                    &mut glyphs.into_iter(),
732                );
733            }
734            Some(TextStrokeStyle::Center) => {
735                let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
736
737                draw_glyphs(
738                    item_renderer,
739                    run.font(),
740                    PhysicalLength::new(run.font_size()),
741                    normalized_coords,
742                    &synthesis,
743                    fill_brush.clone(),
744                    para_y,
745                    &mut glyphs.iter().cloned(),
746                );
747
748                if let Some(stroke_brush) = default_stroke_brush.clone() {
749                    draw_glyphs(
750                        item_renderer,
751                        run.font(),
752                        PhysicalLength::new(run.font_size()),
753                        normalized_coords,
754                        &synthesis,
755                        stroke_brush,
756                        para_y,
757                        &mut glyphs.into_iter(),
758                    );
759                }
760            }
761            None => {
762                draw_glyphs(
763                    item_renderer,
764                    run.font(),
765                    PhysicalLength::new(run.font_size()),
766                    normalized_coords,
767                    &synthesis,
768                    fill_brush.clone(),
769                    para_y,
770                    glyphs_it,
771                );
772            }
773        }
774
775        let metrics = run.metrics();
776
777        if glyph_run.style().underline.is_some() {
778            item_renderer.fill_rectangle(
779                PhysicalRect::new(
780                    PhysicalPoint::from_lengths(
781                        PhysicalLength::new(glyph_run.offset()),
782                        para_y
783                            + PhysicalLength::new(glyph_run.baseline() - metrics.underline_offset),
784                    ),
785                    PhysicalSize::new(glyph_run.advance(), metrics.underline_size),
786                ),
787                fill_brush.clone(),
788            );
789        }
790
791        if glyph_run.style().strikethrough.is_some() {
792            item_renderer.fill_rectangle(
793                PhysicalRect::new(
794                    PhysicalPoint::from_lengths(
795                        PhysicalLength::new(glyph_run.offset()),
796                        para_y
797                            + PhysicalLength::new(
798                                glyph_run.baseline() - metrics.strikethrough_offset,
799                            ),
800                    ),
801                    PhysicalSize::new(glyph_run.advance(), metrics.strikethrough_size),
802                ),
803                fill_brush,
804            );
805        }
806    }
807}
808
809struct Layout {
810    paragraphs: Vec<TextParagraph>,
811    y_offset: PhysicalLength,
812    max_width: PhysicalLength,
813    height: PhysicalLength,
814    max_physical_height: Option<PhysicalLength>,
815    elision_info: Option<ElisionInfo>,
816}
817
818impl Layout {
819    fn paragraph_by_byte_offset(&self, byte_offset: usize) -> Option<&TextParagraph> {
820        self.paragraphs.iter().find(|p| byte_offset >= p.range.start && byte_offset <= p.range.end)
821    }
822
823    fn paragraph_by_y(&self, y: PhysicalLength) -> Option<&TextParagraph> {
824        // Adjust for vertical alignment
825        let y = y - self.y_offset;
826
827        if y < PhysicalLength::zero() {
828            return self.paragraphs.first();
829        }
830
831        let idx = self.paragraphs.binary_search_by(|paragraph| {
832            if y < paragraph.y {
833                core::cmp::Ordering::Greater
834            } else if y >= paragraph.y + PhysicalLength::new(paragraph.layout.height()) {
835                core::cmp::Ordering::Less
836            } else {
837                core::cmp::Ordering::Equal
838            }
839        });
840
841        match idx {
842            Ok(i) => self.paragraphs.get(i),
843            Err(_) => self.paragraphs.last(),
844        }
845    }
846
847    fn selection_geometry(
848        &self,
849        selection_range: Range<usize>,
850        mut callback: impl FnMut(PhysicalRect),
851    ) {
852        for paragraph in &self.paragraphs {
853            let selection_start = selection_range.start.max(paragraph.range.start);
854            let selection_end = selection_range.end.min(paragraph.range.end);
855
856            if selection_start < selection_end {
857                let local_start = selection_start - paragraph.range.start;
858                let local_end = selection_end - paragraph.range.start;
859
860                let selection = parley::editing::Selection::new(
861                    parley::editing::Cursor::from_byte_index(
862                        &paragraph.layout,
863                        local_start,
864                        Default::default(),
865                    ),
866                    parley::editing::Cursor::from_byte_index(
867                        &paragraph.layout,
868                        local_end,
869                        Default::default(),
870                    ),
871                );
872
873                selection.geometry_with(&paragraph.layout, |rect, _| {
874                    callback(PhysicalRect::new(
875                        PhysicalPoint::from_lengths(
876                            PhysicalLength::new(rect.x0 as _),
877                            PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
878                        ),
879                        PhysicalSize::new(rect.width() as _, rect.height() as _),
880                    ));
881                });
882            }
883        }
884    }
885
886    fn byte_offset_from_point(&self, pos: PhysicalPoint) -> usize {
887        let Some(paragraph) = self.paragraph_by_y(pos.y_length()) else {
888            return 0;
889        };
890        let cursor = parley::editing::Cursor::from_point(
891            &paragraph.layout,
892            pos.x,
893            (pos.y_length() - self.y_offset - paragraph.y).get(),
894        );
895        paragraph.range.start + cursor.index()
896    }
897
898    fn cursor_rect_for_byte_offset(
899        &self,
900        byte_offset: usize,
901        cursor_width: PhysicalLength,
902    ) -> PhysicalRect {
903        let Some(paragraph) = self.paragraph_by_byte_offset(byte_offset) else {
904            return PhysicalRect::new(PhysicalPoint::default(), PhysicalSize::new(1.0, 1.0));
905        };
906
907        let local_offset = byte_offset - paragraph.range.start;
908        let cursor = parley::editing::Cursor::from_byte_index(
909            &paragraph.layout,
910            local_offset,
911            Default::default(),
912        );
913        let rect = cursor.geometry(&paragraph.layout, cursor_width.get());
914
915        PhysicalRect::new(
916            PhysicalPoint::from_lengths(
917                PhysicalLength::new(rect.x0 as _),
918                PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
919            ),
920            PhysicalSize::new(rect.width() as _, rect.height() as _),
921        )
922    }
923
924    /// Returns an iterator over the run's glyphs, truncated if necessary to fit within the max width,
925    /// plus an optional elipsis glyph with its font and size to be drawn separately.
926    /// Call this function only for the last line of the layout.
927    fn glyphs_with_elision<'a>(
928        &'a self,
929        glyph_run: &'a parley::layout::GlyphRun<Brush>,
930    ) -> (
931        impl Iterator<Item = parley::layout::Glyph> + Clone + 'a,
932        Option<(parley::layout::Glyph, parley::FontData, PhysicalLength)>,
933    ) {
934        let elipsis_advance =
935            self.elision_info.as_ref().map(|info| info.elipsis_glyph.advance).unwrap_or(0.0);
936        let max_width = self
937            .elision_info
938            .as_ref()
939            .map(|info| info.max_physical_width)
940            .unwrap_or(PhysicalLength::new(f32::MAX));
941
942        let run_start = PhysicalLength::new(glyph_run.offset());
943        let run_end = PhysicalLength::new(glyph_run.offset() + glyph_run.advance());
944
945        // Run starts after where the elipsis would go - skip entirely
946        let run_beyond_elision = run_start > max_width;
947        // Run extends beyond max width and needs truncation + elipsis
948        let needs_elision = !run_beyond_elision && run_end.get().floor() > max_width.get().ceil();
949
950        let truncated_glyphs = glyph_run.positioned_glyphs().take_while(move |glyph| {
951            !run_beyond_elision
952                && (!needs_elision
953                    || PhysicalLength::new(glyph.x + glyph.advance + elipsis_advance) <= max_width)
954        });
955
956        let elipsis = if needs_elision {
957            self.elision_info.as_ref().map(|info| {
958                let elipsis_x = glyph_run
959                    .positioned_glyphs()
960                    .find(|glyph| {
961                        PhysicalLength::new(glyph.x + glyph.advance + info.elipsis_glyph.advance)
962                            > info.max_physical_width
963                    })
964                    .map(|g| g.x)
965                    .unwrap_or(0.0);
966
967                let mut elipsis_glyph = info.elipsis_glyph;
968                elipsis_glyph.x = elipsis_x;
969
970                let font_size = PhysicalLength::new(glyph_run.run().font_size());
971                (elipsis_glyph, info.font_for_elipsis_glyph.clone(), font_size)
972            })
973        } else {
974            None
975        };
976
977        (truncated_glyphs, elipsis)
978    }
979
980    fn draw<R: GlyphRenderer>(
981        &self,
982        item_renderer: &mut R,
983        default_fill_brush: <R as GlyphRenderer>::PlatformBrush,
984        default_stroke_brush: Option<<R as GlyphRenderer>::PlatformBrush>,
985        draw_glyphs: &mut dyn FnMut(
986            &mut R,
987            &parley::FontData,
988            PhysicalLength,
989            &[i16],               // normalized variation coords
990            &fontique::Synthesis, // design-space variation settings
991            <R as GlyphRenderer>::PlatformBrush,
992            PhysicalLength, // y offset for paragraph
993            &mut dyn Iterator<Item = parley::layout::Glyph>,
994        ),
995    ) {
996        for paragraph in &self.paragraphs {
997            paragraph.draw(
998                self,
999                item_renderer,
1000                &default_fill_brush,
1001                &default_stroke_brush,
1002                draw_glyphs,
1003            );
1004        }
1005    }
1006}
1007
1008pub fn draw_text(
1009    item_renderer: &mut impl GlyphRenderer,
1010    text: Pin<&dyn crate::item_rendering::RenderText>,
1011    item_rc: Option<&crate::item_tree::ItemRc>,
1012    size: LogicalSize,
1013    cache: Option<&TextLayoutCache>,
1014) {
1015    let max_width = size.width_length();
1016    let max_height = size.height_length();
1017
1018    if max_width.get() <= 0. || max_height.get() <= 0. {
1019        return;
1020    }
1021
1022    let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size)
1023    else {
1024        // Nothing to draw
1025        return;
1026    };
1027
1028    let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1029
1030    let (stroke_brush, stroke_width, stroke_style) = text.stroke();
1031    let platform_stroke_brush = if !stroke_brush.is_transparent() {
1032        let stroke_width = if stroke_width.get() != 0.0 {
1033            (stroke_width * scale_factor).get()
1034        } else {
1035            // Hairline stroke
1036            1.0
1037        };
1038        let stroke_width = match stroke_style {
1039            TextStrokeStyle::Outside => stroke_width * 2.0,
1040            TextStrokeStyle::Center => stroke_width,
1041        };
1042        item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size)
1043    } else {
1044        None
1045    };
1046
1047    // The layout_builder is still needed for the elision glyph in layout().
1048    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1049        item_rc.map(|item_rc| text.font_request(item_rc)),
1050        text.wrap(),
1051        platform_stroke_brush.is_some().then_some(stroke_style),
1052        scale_factor,
1053    );
1054
1055    let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1056
1057    let mut guard =
1058        get_or_create_text_paragraphs(cache, item_rc, text, scale_factor, &mut font_ctx);
1059
1060    let (horizontal_align, vertical_align) = text.alignment();
1061    let text_overflow = text.overflow();
1062    let text_wrap = text.wrap();
1063
1064    let layout = layout(
1065        &layout_builder,
1066        &mut font_ctx,
1067        guard.paragraphs.take().unwrap_or_default(),
1068        scale_factor,
1069        text_wrap,
1070        LayoutOptions {
1071            horizontal_align,
1072            vertical_align,
1073            max_height: Some(max_height),
1074            max_width: Some(max_width),
1075            text_overflow: text.overflow(),
1076        },
1077    );
1078
1079    drop(font_ctx);
1080
1081    let render = if text_overflow == TextOverflow::Clip {
1082        item_renderer.save_state();
1083
1084        item_renderer.combine_clip(
1085            LogicalRect::new(LogicalPoint::default(), size),
1086            LogicalBorderRadius::zero(),
1087            LogicalLength::zero(),
1088        )
1089    } else {
1090        true
1091    };
1092
1093    if render {
1094        layout.draw(
1095            item_renderer,
1096            platform_fill_brush,
1097            platform_stroke_brush,
1098            &mut |item_renderer: &mut _,
1099                  font,
1100                  font_size,
1101                  normalized_coords,
1102                  synthesis,
1103                  brush,
1104                  y_offset,
1105                  glyphs_it| {
1106                item_renderer.draw_glyph_run(
1107                    font,
1108                    font_size,
1109                    normalized_coords,
1110                    synthesis,
1111                    brush,
1112                    y_offset,
1113                    glyphs_it,
1114                );
1115            },
1116        );
1117    }
1118
1119    if text_overflow == TextOverflow::Clip {
1120        item_renderer.restore_state();
1121    }
1122
1123    // Put paragraphs back into the cache guard for reuse.
1124    // break_all_lines replaces line data each time, so the state is ready for the next call.
1125    guard.paragraphs = Some(layout.paragraphs);
1126}
1127
1128#[cfg(feature = "std")]
1129pub fn link_under_cursor(
1130    font_context: &mut parley::FontContext,
1131    scale_factor: ScaleFactor,
1132    text: Pin<&dyn crate::item_rendering::RenderText>,
1133    item_rc: &crate::item_tree::ItemRc,
1134    size: LogicalSize,
1135    cursor: PhysicalPoint,
1136    cache: Option<&TextLayoutCache>,
1137) -> Option<std::string::String> {
1138    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1139        Some(text.font_request(item_rc)),
1140        text.wrap(),
1141        None,
1142        scale_factor,
1143    );
1144
1145    let mut guard =
1146        get_or_create_text_paragraphs(cache, Some(item_rc), text, scale_factor, font_context);
1147
1148    let (horizontal_align, vertical_align) = text.alignment();
1149
1150    let layout = layout(
1151        &layout_builder,
1152        font_context,
1153        guard.paragraphs.take().unwrap_or_default(),
1154        scale_factor,
1155        text.wrap(),
1156        LayoutOptions {
1157            horizontal_align,
1158            vertical_align,
1159            max_height: Some(size.height_length()),
1160            max_width: Some(size.width_length()),
1161            text_overflow: text.overflow(),
1162        },
1163    );
1164
1165    let result = layout.paragraph_by_y(cursor.y_length()).and_then(|paragraph| {
1166        let paragraph_y: f64 = paragraph.y.cast::<f64>().get();
1167
1168        paragraph
1169            .links
1170            .iter()
1171            .find(|(range, _)| {
1172                let start = parley::editing::Cursor::from_byte_index(
1173                    &paragraph.layout,
1174                    range.start,
1175                    Default::default(),
1176                );
1177                let end = parley::editing::Cursor::from_byte_index(
1178                    &paragraph.layout,
1179                    range.end,
1180                    Default::default(),
1181                );
1182                let mut clicked = false;
1183                let link_range = parley::Selection::new(start, end);
1184                link_range.geometry_with(&paragraph.layout, |mut bounding_box, _line| {
1185                    bounding_box.y0 += paragraph_y;
1186                    bounding_box.y1 += paragraph_y;
1187                    clicked = bounding_box.union(parley::BoundingBox::new(
1188                        cursor.x.into(),
1189                        cursor.y.into(),
1190                        cursor.x.into(),
1191                        cursor.y.into(),
1192                    )) == bounding_box;
1193                });
1194                clicked
1195            })
1196            .map(|(_, link)| link.clone())
1197    });
1198
1199    // Put paragraphs back into the cache guard for reuse.
1200    guard.paragraphs = Some(layout.paragraphs);
1201
1202    result
1203}
1204
1205pub fn draw_text_input(
1206    item_renderer: &mut impl GlyphRenderer,
1207    text_input: Pin<&crate::items::TextInput>,
1208    item_rc: &crate::item_tree::ItemRc,
1209    size: LogicalSize,
1210    password_character: Option<fn() -> char>,
1211) {
1212    let width = size.width_length();
1213    let height = size.height_length();
1214    if width.get() <= 0. || height.get() <= 0. {
1215        return;
1216    }
1217
1218    let visual_representation = text_input.visual_representation(password_character);
1219
1220    let Some(platform_fill_brush) =
1221        item_renderer.platform_text_fill_brush(visual_representation.text_color, size)
1222    else {
1223        return;
1224    };
1225
1226    let selection_range = if !visual_representation.preedit_range.is_empty() {
1227        visual_representation.preedit_range.start..visual_representation.preedit_range.end
1228    } else {
1229        visual_representation.selection_range.start..visual_representation.selection_range.end
1230    };
1231
1232    let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1233
1234    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1235        Some(text_input.font_request(item_rc)),
1236        text_input.wrap(),
1237        None,
1238        scale_factor,
1239    );
1240
1241    let text: SharedString = visual_representation.text.into();
1242
1243    // When a piece of text is first selected, it gets an empty range like `Some(1..1)`.
1244    // If the text starts with a multi-byte character then this selection will be within
1245    // that character and parley will panic. We just filter out empty selection ranges.
1246    let selection_and_color = if !selection_range.is_empty() {
1247        Some((selection_range.clone(), text_input.selection_foreground_color()))
1248    } else {
1249        None
1250    };
1251
1252    let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1253
1254    let paragraphs_without_linebreaks = create_text_paragraphs(
1255        &layout_builder,
1256        &mut font_ctx,
1257        PlainOrStyledText::Plain(text),
1258        selection_and_color,
1259        Color::default(),
1260    );
1261
1262    let layout = layout(
1263        &layout_builder,
1264        &mut font_ctx,
1265        paragraphs_without_linebreaks,
1266        scale_factor,
1267        text_input.wrap(),
1268        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1269    );
1270
1271    drop(font_ctx);
1272
1273    layout.selection_geometry(selection_range, |selection_rect| {
1274        item_renderer
1275            .fill_rectange_with_color(selection_rect, text_input.selection_background_color());
1276    });
1277
1278    item_renderer.save_state();
1279
1280    let render = item_renderer.combine_clip(
1281        LogicalRect::new(LogicalPoint::default(), size),
1282        LogicalBorderRadius::zero(),
1283        LogicalLength::zero(),
1284    );
1285
1286    if render {
1287        layout.draw(
1288            item_renderer,
1289            platform_fill_brush,
1290            None,
1291            &mut |item_renderer: &mut _,
1292                  font,
1293                  font_size,
1294                  normalized_coords,
1295                  synthesis,
1296                  brush,
1297                  y_offset,
1298                  glyphs_it| {
1299                item_renderer.draw_glyph_run(
1300                    font,
1301                    font_size,
1302                    normalized_coords,
1303                    synthesis,
1304                    brush,
1305                    y_offset,
1306                    glyphs_it,
1307                );
1308            },
1309        );
1310
1311        if let Some(cursor_pos) = visual_representation.cursor_position {
1312            let cursor_rect = layout.cursor_rect_for_byte_offset(
1313                cursor_pos,
1314                text_input.text_cursor_width() * scale_factor,
1315            );
1316            item_renderer.fill_rectange_with_color(cursor_rect, visual_representation.cursor_color);
1317        }
1318    }
1319
1320    item_renderer.restore_state();
1321}
1322
1323pub fn text_size(
1324    renderer: &dyn RendererSealed,
1325    text_item: Pin<&dyn crate::item_rendering::RenderString>,
1326    item_rc: &crate::item_tree::ItemRc,
1327    max_width: Option<LogicalLength>,
1328    text_wrap: TextWrap,
1329    _cache: Option<&TextLayoutCache>,
1330) -> Option<LogicalSize> {
1331    let scale_factor = renderer.scale_factor()?;
1332
1333    let ctx = renderer.slint_context()?;
1334    let mut font_ctx = ctx.font_context().borrow_mut();
1335
1336    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1337        Some(text_item.font_request(item_rc)),
1338        text_wrap,
1339        None,
1340        scale_factor,
1341    );
1342
1343    let text = text_item.text();
1344
1345    let paragraphs_without_linebreaks =
1346        create_text_paragraphs(&layout_builder, &mut font_ctx, text, None, Color::default());
1347
1348    let layout = layout(
1349        &layout_builder,
1350        &mut font_ctx,
1351        paragraphs_without_linebreaks,
1352        scale_factor,
1353        text_wrap,
1354        LayoutOptions {
1355            max_width,
1356            max_height: None,
1357            horizontal_align: TextHorizontalAlignment::Left,
1358            vertical_align: TextVerticalAlignment::Top,
1359            text_overflow: TextOverflow::Clip,
1360        },
1361    );
1362    Some(PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor)
1363}
1364
1365pub fn char_size(
1366    font_ctx: &mut parley::FontContext,
1367    text_item: Pin<&dyn crate::item_rendering::HasFont>,
1368    item_rc: &crate::item_tree::ItemRc,
1369    ch: char,
1370) -> Option<LogicalSize> {
1371    let font_request = text_item.font_request(item_rc);
1372    let font = font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
1373
1374    let char_map = font.charmap()?;
1375
1376    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1377
1378    let glyph_index = char_map.map(ch)?;
1379
1380    let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE);
1381
1382    let location = face.axes().location(font.synthesis.variation_settings());
1383
1384    let glyph_metrics = skrifa::metrics::GlyphMetrics::new(
1385        &face,
1386        skrifa::instance::Size::new(pixel_size.get()),
1387        &location,
1388    );
1389
1390    let advance_width = LogicalLength::new(glyph_metrics.advance_width(glyph_index.into())?);
1391
1392    let font_metrics = skrifa::metrics::Metrics::new(
1393        &face,
1394        skrifa::instance::Size::new(pixel_size.get()),
1395        &location,
1396    );
1397
1398    Some(LogicalSize::from_lengths(
1399        advance_width,
1400        LogicalLength::new(font_metrics.ascent - font_metrics.descent),
1401    ))
1402}
1403
1404pub fn font_metrics(
1405    font_ctx: &mut parley::FontContext,
1406    font_request: FontRequest,
1407) -> crate::items::FontMetrics {
1408    let logical_pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE).get();
1409
1410    let Some(font) =
1411        font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)
1412    else {
1413        return crate::items::FontMetrics::default();
1414    };
1415
1416    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1417    let location = face.axes().location(font.synthesis.variation_settings());
1418    let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
1419
1420    let units_per_em = metrics.units_per_em as f32;
1421
1422    crate::items::FontMetrics {
1423        ascent: metrics.ascent * logical_pixel_size / units_per_em,
1424        descent: metrics.descent * logical_pixel_size / units_per_em,
1425        x_height: metrics.x_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1426        cap_height: metrics.cap_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1427    }
1428}
1429
1430pub fn text_input_byte_offset_for_position(
1431    renderer: &dyn RendererSealed,
1432    text_input: Pin<&crate::items::TextInput>,
1433    item_rc: &crate::item_tree::ItemRc,
1434    pos: LogicalPoint,
1435) -> usize {
1436    let Some(scale_factor) = renderer.scale_factor() else {
1437        return 0;
1438    };
1439    let pos: PhysicalPoint = pos * scale_factor;
1440
1441    let width = text_input.width();
1442    let height = text_input.height();
1443    if width.get() <= 0. || height.get() <= 0. || pos.y < 0. {
1444        return 0;
1445    }
1446
1447    let Some(ctx) = renderer.slint_context() else {
1448        return 0;
1449    };
1450    let mut font_ctx = ctx.font_context().borrow_mut();
1451
1452    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1453        Some(text_input.font_request(item_rc)),
1454        text_input.wrap(),
1455        None,
1456        scale_factor,
1457    );
1458
1459    let text = text_input.text();
1460    let paragraphs_without_linebreaks = create_text_paragraphs(
1461        &layout_builder,
1462        &mut font_ctx,
1463        PlainOrStyledText::Plain(text),
1464        None,
1465        Color::default(),
1466    );
1467
1468    let layout = layout(
1469        &layout_builder,
1470        &mut font_ctx,
1471        paragraphs_without_linebreaks,
1472        scale_factor,
1473        text_input.wrap(),
1474        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1475    );
1476    let byte_offset = layout.byte_offset_from_point(pos);
1477    let visual_representation = text_input.visual_representation(None);
1478    visual_representation.map_byte_offset_from_byte_offset_in_visual_text(byte_offset)
1479}
1480
1481pub fn text_input_cursor_rect_for_byte_offset(
1482    renderer: &dyn RendererSealed,
1483    text_input: Pin<&crate::items::TextInput>,
1484    item_rc: &crate::item_tree::ItemRc,
1485    byte_offset: usize,
1486) -> LogicalRect {
1487    let Some(scale_factor) = renderer.scale_factor() else {
1488        return LogicalRect::default();
1489    };
1490
1491    let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1492        Some(text_input.font_request(item_rc)),
1493        text_input.wrap(),
1494        None,
1495        scale_factor,
1496    );
1497
1498    let width = text_input.width();
1499    let height = text_input.height();
1500    if width.get() <= 0. || height.get() <= 0. {
1501        return LogicalRect::new(
1502            LogicalPoint::default(),
1503            LogicalSize::from_lengths(LogicalLength::new(1.0), layout_builder.pixel_size),
1504        );
1505    }
1506
1507    let Some(ctx) = renderer.slint_context() else {
1508        return LogicalRect::default();
1509    };
1510    let mut font_ctx = ctx.font_context().borrow_mut();
1511
1512    let text = text_input.text();
1513    let paragraphs_without_linebreaks = create_text_paragraphs(
1514        &layout_builder,
1515        &mut font_ctx,
1516        PlainOrStyledText::Plain(text),
1517        None,
1518        Color::default(),
1519    );
1520
1521    let layout = layout(
1522        &layout_builder,
1523        &mut font_ctx,
1524        paragraphs_without_linebreaks,
1525        scale_factor,
1526        text_input.wrap(),
1527        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1528    );
1529    let cursor_rect = layout
1530        .cursor_rect_for_byte_offset(byte_offset, text_input.text_cursor_width() * scale_factor);
1531    cursor_rect / scale_factor
1532}