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 unshareable
5pub use parley;
6pub use parley::fontique;
7
8use crate::{
9    Color,
10    graphics::FontRequest,
11    item_rendering::PlainOrStyledText,
12    items::TextStrokeStyle,
13    lengths::{
14        LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx,
15        PointLengths, RectLengths, ScaleFactor, SizeLengths,
16    },
17    renderer::RendererSealed,
18    textlayout::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap},
19    window::WindowAdapter,
20};
21use alloc::rc::Rc;
22use alloc::vec::Vec;
23use core::ops::Range;
24use core::pin::Pin;
25use euclid::num::Zero;
26use i_slint_common::sharedfontique;
27use skrifa::MetadataProvider as _;
28use std::cell::RefCell;
29use std::collections::HashSet;
30use std::sync::Arc;
31
32#[derive(derive_more::Deref, derive_more::DerefMut)]
33pub struct FontContext {
34    #[deref]
35    #[deref_mut]
36    pub inner: parley::FontContext,
37    /// `(ptr, len)` of each `&'static [u8]` already handed to fontique, so repeat
38    /// `register_static_font` calls for the same embedded font are skipped.
39    registered_static_fonts: HashSet<(usize, usize)>,
40}
41
42impl FontContext {
43    pub fn new(inner: parley::FontContext) -> Self {
44        Self { inner, registered_static_fonts: HashSet::default() }
45    }
46
47    pub fn register_static_font(&mut self, data: &'static [u8]) {
48        let key = (data.as_ptr() as usize, data.len());
49        if self.registered_static_fonts.insert(key) {
50            self.inner.collection.register_fonts(fontique::Blob::new(Arc::new(data)), None);
51        }
52    }
53
54    pub fn clear_registered_static_fonts(&mut self) {
55        self.registered_static_fonts.clear();
56    }
57}
58
59pub type PhysicalLength = euclid::Length<f32, PhysicalPx>;
60pub type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
61type PhysicalSize = euclid::Size2D<f32, PhysicalPx>;
62type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>;
63
64pub use super::DEFAULT_FONT_SIZE;
65
66mod cache;
67mod draw;
68mod layout;
69mod selection;
70mod shaping;
71#[cfg(test)]
72mod tests;
73
74pub use cache::TextLayoutCache;
75pub use draw::{GlyphRenderer, RectangleBorder};
76
77use cache::cached_paragraphs;
78use layout::{Layout, LayoutOptions, layout};
79use selection::{SelectionRendering, SelectionSpans};
80use shaping::{
81    Brush, LayoutWithoutLineBreaksBuilder, create_text_paragraphs, shape_paragraphs,
82    shaping_builder,
83};
84
85/// Lays out the shaped text of an item and runs `f` over the result.
86///
87/// This is the one place that checks paragraphs out of a [`TextLayoutCache`] entry and hands them
88/// back: `f` only borrows the [`Layout`], so no caller can lose the shaped paragraphs and cost the
89/// next use of the entry a reshape.
90///
91/// The font context is borrowed from `window`'s Slint context for shaping and layout only, and
92/// released before `f` runs -- glyph drawing inside `f` re-enters it, and property bindings
93/// evaluated under `f` must not find it borrowed. The wrap mode and scale factor the cache entry
94/// is keyed on come from `layout_builder`, so shaping and layout cannot disagree about either.
95///
96/// Returns `None` only when `window` has no Slint context yet.
97fn with_text_layout<R>(
98    cache: Option<&TextLayoutCache>,
99    item_rc: Option<&crate::item_tree::ItemRc>,
100    text: Pin<&dyn crate::item_rendering::RenderString>,
101    layout_builder: &LayoutWithoutLineBreaksBuilder,
102    options: LayoutOptions,
103    window: &crate::api::Window,
104    f: impl FnOnce(&Layout) -> R,
105) -> Option<R> {
106    let ctx = crate::window::WindowInner::from_pub(window).try_context()?;
107    let mut font_ctx = ctx.font_context().borrow_mut();
108
109    let text_wrap = layout_builder.text_wrap;
110    let scale_factor = layout_builder.scale_factor;
111    let mut guard =
112        cached_paragraphs(cache, item_rc, text_wrap, window, &mut font_ctx, &|font_context| {
113            shape_paragraphs(text, item_rc, text_wrap, scale_factor, font_context)
114        });
115
116    let line_breaking = guard.take_line_breaking();
117    let layout =
118        layout(layout_builder, &mut font_ctx, guard.take(), scale_factor, options, line_breaking);
119    drop(font_ctx);
120
121    if layout.broke_lines {
122        #[cfg(feature = "testing")]
123        if let Some(cache) = cache {
124            cache.count_layout_miss();
125        }
126    }
127
128    let result = f(&layout);
129    let (paragraphs, line_breaking) = layout.dismantle();
130    guard.restore(paragraphs, line_breaking);
131    Some(result)
132}
133
134pub fn draw_text(
135    item_renderer: &mut impl GlyphRenderer,
136    text: Pin<&dyn crate::item_rendering::RenderText>,
137    item_rc: Option<&crate::item_tree::ItemRc>,
138    size: LogicalSize,
139    cache: Option<&TextLayoutCache>,
140) {
141    let max_width = size.width_length();
142    let max_height = size.height_length();
143
144    if max_width.get() <= 0. || max_height.get() <= 0. {
145        return;
146    }
147
148    let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size)
149    else {
150        // Nothing to draw
151        return;
152    };
153
154    let scale_factor = item_renderer.scale_factor();
155
156    let (stroke_brush, stroke_width, stroke_style) = text.stroke();
157    let platform_stroke_brush = if !stroke_brush.is_transparent() {
158        let stroke_width = if stroke_width.get() != 0.0 {
159            (stroke_width * scale_factor).get()
160        } else {
161            // Hairline stroke
162            1.0
163        };
164        let stroke_width = match stroke_style {
165            TextStrokeStyle::Outside => stroke_width * 2.0,
166            TextStrokeStyle::Center => stroke_width,
167        };
168        item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size)
169    } else {
170        None
171    };
172
173    let layout_builder = shaping_builder(text, item_rc, text.wrap(), scale_factor);
174
175    let window_adapter = item_renderer.window().window_adapter();
176
177    let (horizontal_align, vertical_align) = text.alignment();
178    let text_overflow = text.overflow();
179    let text_color = text.color().color();
180
181    let _ = with_text_layout(
182        cache,
183        item_rc,
184        text,
185        &layout_builder,
186        LayoutOptions {
187            horizontal_align,
188            vertical_align,
189            max_height: Some(max_height),
190            max_width: Some(max_width),
191            max_lines: text.line_limit(),
192            text_overflow,
193        },
194        window_adapter.window(),
195        |layout| {
196            // When `overflow: elide` can't even fit the first line, the line is still drawn
197            // (rather than dropped, which would render nothing) but its vertical overflow needs
198            // to be clipped like `overflow: clip` would. Horizontal elision still applies, so a
199            // line that is both too tall and too wide is clipped vertically and gets an ellipsis
200            // horizontally.
201            let clip_overflowing_first_line =
202                text_overflow == TextOverflow::Elide && layout.first_line_exceeds_height();
203
204            let render = if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
205                item_renderer.save_state();
206
207                item_renderer.combine_clip(
208                    LogicalRect::new(LogicalPoint::default(), size),
209                    LogicalBorderRadius::zero(),
210                )
211            } else {
212                true
213            };
214
215            if render {
216                layout.draw(
217                    item_renderer,
218                    platform_fill_brush,
219                    platform_stroke_brush,
220                    text_color,
221                    // `Text` has no selection today; the machinery is shared so wiring one up
222                    // later is a matter of passing spans here.
223                    None,
224                );
225            }
226
227            if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
228                item_renderer.restore_state();
229            }
230        },
231    );
232}
233
234#[cfg(feature = "std")]
235pub fn link_under_cursor(
236    scale_factor: ScaleFactor,
237    text: Pin<&dyn crate::item_rendering::RenderText>,
238    item_rc: &crate::item_tree::ItemRc,
239    size: LogicalSize,
240    cursor: PhysicalPoint,
241    window: &crate::api::Window,
242    cache: Option<&TextLayoutCache>,
243) -> Option<std::string::String> {
244    let layout_builder = shaping_builder(text, Some(item_rc), text.wrap(), scale_factor);
245
246    let (horizontal_align, vertical_align) = text.alignment();
247
248    with_text_layout(
249        cache,
250        Some(item_rc),
251        text,
252        &layout_builder,
253        LayoutOptions {
254            horizontal_align,
255            vertical_align,
256            max_height: Some(size.height_length()),
257            max_width: Some(size.width_length()),
258            max_lines: text.line_limit(),
259            text_overflow: text.overflow(),
260        },
261        window,
262        |layout| link_in_layout(layout, cursor),
263    )
264    .flatten()
265}
266
267fn link_in_layout(layout: &Layout, cursor: PhysicalPoint) -> Option<std::string::String> {
268    layout.paragraph_by_y(cursor.y_length()).and_then(|paragraph| {
269        let paragraph_y: f64 = paragraph.y.cast::<f64>().get();
270
271        paragraph
272            .links
273            .iter()
274            .find(|(range, _)| {
275                let start = parley::editing::Cursor::from_byte_index(
276                    &paragraph.layout,
277                    range.start,
278                    Default::default(),
279                );
280                let end = parley::editing::Cursor::from_byte_index(
281                    &paragraph.layout,
282                    range.end,
283                    Default::default(),
284                );
285                let mut clicked = false;
286                let link_range = parley::Selection::new(start, end);
287                link_range.geometry_with(&paragraph.layout, |mut bounding_box, _line| {
288                    bounding_box.y0 += paragraph_y;
289                    bounding_box.y1 += paragraph_y;
290                    clicked = bounding_box.union(parley::BoundingBox::new(
291                        cursor.x.into(),
292                        cursor.y.into(),
293                        cursor.x.into(),
294                        cursor.y.into(),
295                    )) == bounding_box;
296                });
297                clicked
298            })
299            .map(|(_, link)| link.clone())
300    })
301}
302
303pub fn draw_text_input(
304    item_renderer: &mut impl GlyphRenderer,
305    text_input: Pin<&crate::items::TextInput>,
306    item_rc: &crate::item_tree::ItemRc,
307    size: LogicalSize,
308    cache: &TextLayoutCache,
309) {
310    let width = size.width_length();
311    let height = size.height_length();
312    if width.get() <= 0. || height.get() <= 0. {
313        return;
314    }
315
316    let visual_representation = text_input.visual_representation();
317
318    let text_color = visual_representation.text_color.color();
319    let Some(platform_fill_brush) =
320        item_renderer.platform_text_fill_brush(visual_representation.text_color.clone(), size)
321    else {
322        return;
323    };
324
325    let selection_range = if !visual_representation.preedit_range.is_empty() {
326        visual_representation.preedit_range.start..visual_representation.preedit_range.end
327    } else {
328        visual_representation.selection_range.start..visual_representation.selection_range.end
329    };
330
331    let scale_factor = item_renderer.scale_factor();
332
333    let layout_builder =
334        shaping_builder(text_input, Some(item_rc), text_input.wrap(), scale_factor);
335
336    let window_adapter = item_renderer.window().window_adapter();
337
338    // The visual text shapes through the shared cache entry like any other text: a selection
339    // doesn't make the entry unshareable, because it is applied when drawing, by clipping the
340    // runs it cuts across, and never reaches shaping. Cluster ranges and advances are identical
341    // with and without one, so a selected `TextInput` hits the same entry as an unselected one --
342    // which is what keeps dragging a selection, or composing with an IME, from re-shaping the
343    // document on every event. A password field shapes a substituted text, but the substitution
344    // is the same everywhere, so it is cacheable too.
345    let _ = with_text_layout(
346        Some(cache),
347        Some(item_rc),
348        text_input,
349        &layout_builder,
350        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
351        window_adapter.window(),
352        |layout| {
353            item_renderer.save_state();
354
355            let render = item_renderer.combine_clip(
356                LogicalRect::new(LogicalPoint::default(), size),
357                LogicalBorderRadius::zero(),
358            );
359
360            if render {
361                // When a piece of text is first selected, it gets an empty range like `1..1`. If
362                // the text starts with a multi-byte character then this selection would be within
363                // that character and parley would panic, so empty ranges are filtered out. The
364                // spans only feed drawing (the highlight fill and the glyph clip), so only the
365                // lines the clip lets through need any.
366                let selection_spans = if selection_range.is_empty() {
367                    SelectionSpans::default()
368                } else {
369                    layout.selection_geometry(selection_range, &draw::visible_band(item_renderer))
370                };
371                // Inside the clip, like the glyphs it sits under: a line box taller than the item
372                // would otherwise paint the highlight over whatever follows the input.
373                for background in selection_spans.backgrounds() {
374                    item_renderer.fill_rectangle_with_color(
375                        background,
376                        text_input.selection_background_color(),
377                    );
378                }
379
380                // Selected glyphs are recolored by clipping, not by restyling the layout, so that
381                // a boundary landing inside a ligature cuts the glyph instead of recoloring all
382                // of it.
383                let selection = (!selection_spans.is_empty())
384                    .then(|| {
385                        item_renderer
386                            .platform_brush_for_color(&text_input.selection_foreground_color())
387                            .map(|foreground| SelectionRendering {
388                                spans: &selection_spans,
389                                foreground,
390                            })
391                    })
392                    .flatten();
393
394                layout.draw(
395                    item_renderer,
396                    platform_fill_brush,
397                    None,
398                    text_color,
399                    selection.as_ref(),
400                );
401
402                if let Some(cursor_pos) = visual_representation.cursor_position {
403                    let cursor_rect = layout.cursor_rect_for_byte_offset(
404                        cursor_pos,
405                        visual_representation.cursor_affinity,
406                        text_input.text_cursor_width() * scale_factor,
407                    );
408                    item_renderer
409                        .fill_rectangle_with_color(cursor_rect, visual_representation.cursor_color);
410                }
411            }
412
413            item_renderer.restore_state();
414        },
415    );
416}
417
418// The public entry points taking a renderer are generic so that RendererSealed's default
419// implementations can pass self. Each is a thin shim that extracts what it needs from the
420// renderer and forwards to a monomorphic inner function, so that the layout code is not
421// instantiated (and duplicated in the binary) once per renderer type.
422pub fn text_size(
423    renderer: &(impl RendererSealed + ?Sized),
424    text_item: Pin<&dyn crate::item_rendering::RenderString>,
425    item_rc: &crate::item_tree::ItemRc,
426    max_width: Option<LogicalLength>,
427    text_wrap: TextWrap,
428    cache: Option<&TextLayoutCache>,
429) -> Option<LogicalSize> {
430    text_size_impl(
431        renderer.scale_factor(),
432        renderer.window_adapter(),
433        text_item,
434        item_rc,
435        max_width,
436        text_wrap,
437        cache,
438    )
439}
440
441fn text_size_impl(
442    scale_factor: Option<ScaleFactor>,
443    window_adapter: Option<Rc<dyn WindowAdapter>>,
444    text_item: Pin<&dyn crate::item_rendering::RenderString>,
445    item_rc: &crate::item_tree::ItemRc,
446    max_width: Option<LogicalLength>,
447    text_wrap: TextWrap,
448    cache: Option<&TextLayoutCache>,
449) -> Option<LogicalSize> {
450    let scale_factor = scale_factor?;
451
452    // Evaluate the properties that `shape_paragraphs` reads before borrowing font_context: they
453    // can trigger property bindings that re-enter text_size for other elements, which would panic
454    // on a second borrow_mut(). Afterwards they are clean, so shaping can read them again -- now
455    // without re-entering -- inside the cache entry's dependency tracker.
456    let _ = text_item.font_request(item_rc);
457    let _ = text_item.stroke();
458    let _ = text_item.link_color();
459    let _ = text_item.text();
460
461    let window_adapter = window_adapter?;
462
463    // Only `layout()`'s elision glyph reads this, and `TextOverflow::Clip` never asks for one.
464    let layout_builder = shaping_builder(text_item, Some(item_rc), text_wrap, scale_factor);
465
466    with_text_layout(
467        cache,
468        Some(item_rc),
469        text_item,
470        &layout_builder,
471        LayoutOptions {
472            max_width,
473            max_height: None,
474            max_lines: text_item.line_limit(),
475            horizontal_align: TextHorizontalAlignment::Left,
476            vertical_align: TextVerticalAlignment::Top,
477            text_overflow: TextOverflow::Clip,
478        },
479        window_adapter.window(),
480        |layout| PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor,
481    )
482}
483
484/// The content widths of the text. See [`crate::renderer::ContentWidths`].
485pub fn text_content_widths(
486    renderer: &(impl RendererSealed + ?Sized),
487    text_item: Pin<&dyn crate::item_rendering::RenderString>,
488    item_rc: &crate::item_tree::ItemRc,
489    cache: Option<&TextLayoutCache>,
490) -> Option<crate::renderer::ContentWidths> {
491    text_content_widths_impl(
492        renderer.scale_factor(),
493        renderer.window_adapter(),
494        renderer.slint_context(),
495        text_item,
496        item_rc,
497        cache,
498    )
499}
500
501fn text_content_widths_impl(
502    scale_factor: Option<ScaleFactor>,
503    window_adapter: Option<Rc<dyn WindowAdapter>>,
504    ctx: Option<crate::SlintContext>,
505    text_item: Pin<&dyn crate::item_rendering::RenderString>,
506    item_rc: &crate::item_tree::ItemRc,
507    cache: Option<&TextLayoutCache>,
508) -> Option<crate::renderer::ContentWidths> {
509    let scale_factor = scale_factor?;
510
511    // See text_size(): evaluate properties before borrowing font_context. Afterwards they are
512    // clean, so `compute` can read them again -- now without re-entering -- inside the cache
513    // entry's dependency tracker.
514    let _ = text_item.font_request(item_rc);
515    let _ = text_item.text();
516
517    let ctx = ctx?;
518    let mut font_ctx = ctx.font_context().borrow_mut();
519
520    // Reads the text, the font request and the line limit, so the cache entry depends on all
521    // three. Nothing else feeds the widths: the wrap mode, the width and the color don't.
522    let compute = |font_ctx: &mut parley::FontContext| {
523        let layout_builder =
524            shaping::content_widths_builder(text_item.font_request(item_rc), scale_factor);
525
526        let paragraphs_without_linebreaks =
527            create_text_paragraphs(&layout_builder, font_ctx, text_item.text(), Color::default());
528
529        // No line breaking needed: parley derives the content widths from the break
530        // opportunities. Paragraphs stack vertically, so both widths are the widest.
531        // Without wrapping each paragraph is one line, so a line limit drops the paragraphs
532        // that are not drawn, from both widths.
533        let (min, max) = paragraphs_without_linebreaks
534            .iter()
535            .take(text_item.line_limit().unwrap_or(usize::MAX))
536            .fold((0., 0.), |(min, max), p| {
537                let w = p.layout.calculate_content_widths();
538                (f32::max(min, w.min), f32::max(max, w.max))
539            });
540        crate::renderer::ContentWidths {
541            min: PhysicalLength::new(min) / scale_factor,
542            max: PhysicalLength::new(max) / scale_factor,
543        }
544    };
545
546    // Without a window there is no way to notice a scale factor change, and the shaped
547    // advances are in physical pixels, so an entry kept across one would be wrong.
548    let Some((cache, window_adapter)) = cache.zip(window_adapter) else {
549        return Some(compute(&mut font_ctx));
550    };
551    cache.clear_if_scale_factor_changed(window_adapter.window());
552    Some(cache.content_widths(item_rc, || compute(&mut font_ctx)))
553}
554
555pub fn char_size(
556    font_ctx: &mut parley::FontContext,
557    text_item: Pin<&dyn crate::item_rendering::HasFont>,
558    item_rc: &crate::item_tree::ItemRc,
559    ch: char,
560) -> Option<LogicalSize> {
561    let font_request = text_item.font_request(item_rc);
562    let font = font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
563
564    let char_map = font.charmap()?;
565
566    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
567
568    let glyph_index = char_map.map(ch)?;
569
570    let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE);
571
572    let location = face.axes().location(font.synthesis.variation_settings());
573
574    let glyph_metrics = skrifa::metrics::GlyphMetrics::new(
575        &face,
576        skrifa::instance::Size::new(pixel_size.get()),
577        &location,
578    );
579
580    let advance_width = LogicalLength::new(glyph_metrics.advance_width(glyph_index.into())?);
581
582    let font_metrics = skrifa::metrics::Metrics::new(
583        &face,
584        skrifa::instance::Size::new(pixel_size.get()),
585        &location,
586    );
587    let natural_line_height = font_metrics.ascent - font_metrics.descent;
588    let line_height = font_request
589        .line_height_for_natural_height(natural_line_height)
590        .unwrap_or(natural_line_height);
591
592    Some(LogicalSize::from_lengths(advance_width, LogicalLength::new(line_height)))
593}
594
595/// The height of one line of text: what a shaped single-line layout reports.
596pub fn text_line_height(
597    font_ctx: &mut parley::FontContext,
598    font_request: &FontRequest,
599) -> Option<LogicalLength> {
600    let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE);
601    shaping::line_height_ratio(font_ctx, font_request).map(|ratio| pixel_size * ratio)
602}
603
604pub fn font_metrics(
605    font_ctx: &mut parley::FontContext,
606    font_request: FontRequest,
607) -> crate::items::FontMetrics {
608    let logical_pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE).get();
609
610    let Some(font) =
611        font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)
612    else {
613        return crate::items::FontMetrics::default();
614    };
615
616    let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
617    let location = face.axes().location(font.synthesis.variation_settings());
618    let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
619
620    let units_per_em = metrics.units_per_em as f32;
621
622    crate::items::FontMetrics {
623        ascent: metrics.ascent * logical_pixel_size / units_per_em,
624        descent: metrics.descent * logical_pixel_size / units_per_em,
625        x_height: metrics.x_height.unwrap_or_default() * logical_pixel_size / units_per_em,
626        cap_height: metrics.cap_height.unwrap_or_default() * logical_pixel_size / units_per_em,
627    }
628}
629
630pub fn text_input_byte_offset_for_position(
631    renderer: &(impl RendererSealed + ?Sized),
632    text_input: Pin<&crate::items::TextInput>,
633    item_rc: &crate::item_tree::ItemRc,
634    pos: LogicalPoint,
635    cache: Option<&TextLayoutCache>,
636) -> (usize, crate::items::TextCursorAffinity) {
637    text_input_byte_offset_for_position_impl(
638        renderer.scale_factor(),
639        renderer.window_adapter(),
640        text_input,
641        item_rc,
642        pos,
643        cache,
644    )
645}
646
647fn text_input_byte_offset_for_position_impl(
648    scale_factor: Option<ScaleFactor>,
649    window_adapter: Option<Rc<dyn WindowAdapter>>,
650    text_input: Pin<&crate::items::TextInput>,
651    item_rc: &crate::item_tree::ItemRc,
652    pos: LogicalPoint,
653    cache: Option<&TextLayoutCache>,
654) -> (usize, crate::items::TextCursorAffinity) {
655    let no_hit = (0, crate::items::TextCursorAffinity::NextCharacter);
656    let Some(scale_factor) = scale_factor else {
657        return no_hit;
658    };
659    let pos: PhysicalPoint = pos * scale_factor;
660
661    let width = text_input.width();
662    let height = text_input.height();
663    if width.get() <= 0. || height.get() <= 0. || pos.y < 0. {
664        return no_hit;
665    }
666
667    let layout_builder =
668        shaping_builder(text_input, Some(item_rc), text_input.wrap(), scale_factor);
669    let visual_representation = text_input.visual_representation();
670
671    let Some(window_adapter) = window_adapter else {
672        return no_hit;
673    };
674
675    let (byte_offset, affinity) = with_text_layout(
676        cache,
677        Some(item_rc),
678        text_input,
679        &layout_builder,
680        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
681        window_adapter.window(),
682        |layout| layout.byte_offset_from_point(pos),
683    )
684    .unwrap_or(no_hit);
685    (visual_representation.map_byte_offset_from_visual_text_to_actual_text(byte_offset), affinity)
686}
687
688pub fn text_input_cursor_rect_for_byte_offset(
689    renderer: &(impl RendererSealed + ?Sized),
690    text_input: Pin<&crate::items::TextInput>,
691    item_rc: &crate::item_tree::ItemRc,
692    byte_offset: usize,
693    affinity: crate::items::TextCursorAffinity,
694    cache: Option<&TextLayoutCache>,
695) -> LogicalRect {
696    text_input_cursor_rect_for_byte_offset_impl(
697        renderer.scale_factor(),
698        renderer.window_adapter(),
699        text_input,
700        item_rc,
701        byte_offset,
702        affinity,
703        cache,
704    )
705}
706
707fn text_input_cursor_rect_for_byte_offset_impl(
708    scale_factor: Option<ScaleFactor>,
709    window_adapter: Option<Rc<dyn WindowAdapter>>,
710    text_input: Pin<&crate::items::TextInput>,
711    item_rc: &crate::item_tree::ItemRc,
712    byte_offset: usize,
713    affinity: crate::items::TextCursorAffinity,
714    cache: Option<&TextLayoutCache>,
715) -> LogicalRect {
716    let Some(scale_factor) = scale_factor else {
717        return LogicalRect::default();
718    };
719
720    let layout_builder =
721        shaping_builder(text_input, Some(item_rc), text_input.wrap(), scale_factor);
722
723    let width = text_input.width();
724    let height = text_input.height();
725    if width.get() <= 0. || height.get() <= 0. {
726        return LogicalRect::new(
727            LogicalPoint::default(),
728            LogicalSize::from_lengths(LogicalLength::new(1.0), layout_builder.pixel_size),
729        );
730    }
731
732    let visual_representation = text_input.visual_representation();
733    let cursor_width = text_input.text_cursor_width() * scale_factor;
734
735    let Some(window_adapter) = window_adapter else {
736        return LogicalRect::default();
737    };
738
739    let byte_offset = visual_representation.map_byte_offset_from_actual_to_visual_text(byte_offset);
740
741    with_text_layout(
742        cache,
743        Some(item_rc),
744        text_input,
745        &layout_builder,
746        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
747        window_adapter.window(),
748        |layout| {
749            layout.cursor_rect_for_byte_offset(byte_offset, affinity, cursor_width) / scale_factor
750        },
751    )
752    .unwrap_or_default()
753}
754
755/// A `TextInput`'s laid-out text, lent to [`with_text_input_layout`]'s callback for one call.
756#[allow(dead_code)]
757pub struct TextInputLayout<'a> {
758    layout: &'a Layout,
759    /// The string the paragraphs were shaped from, which [`TextInputParagraph::range`] indexes.
760    text: &'a str,
761}
762
763#[allow(dead_code)]
764impl<'a> TextInputLayout<'a> {
765    /// The paragraphs, top to bottom. A hard line break separates two of them and belongs to
766    /// neither, since Slint splits the text at `\n` before shaping.
767    pub(crate) fn paragraphs(&self) -> impl Iterator<Item = TextInputParagraph<'a>> {
768        let (text, y_offset) = (self.text, self.layout.y_offset);
769        self.layout.paragraphs.iter().map(move |para| TextInputParagraph {
770            range: para.range.clone(),
771            text: &text[para.range.clone()],
772            layout: &para.layout,
773            y: y_offset + para.y,
774        })
775    }
776}
777
778/// One paragraph of a [`TextInputLayout`].
779#[allow(dead_code)]
780pub(crate) struct TextInputParagraph<'a> {
781    /// Byte range within [`TextInputLayout::text`].
782    range: Range<usize>,
783    /// The slice of that text this paragraph covers.
784    text: &'a str,
785    /// Its shaped, line-broken and aligned glyphs.
786    layout: &'a parley::Layout<Brush>,
787    /// Physical y of its top edge, relative to the item's.
788    y: PhysicalLength,
789}
790
791/// Lays `text_input` out the way `renderer` draws it and lends the result to `f`.
792///
793/// `f` must not lay text out itself: the cache entry stays checked out for the call, and
794/// re-entering it panics.
795///
796/// Returns `None` if the renderer lays no text out through parley, so the caller can tell that
797/// apart from an empty layout.
798pub fn with_text_input_layout<R>(
799    renderer: &(impl RendererSealed + ?Sized),
800    text_input: Pin<&crate::items::TextInput>,
801    item_rc: &crate::item_tree::ItemRc,
802    size: LogicalSize,
803    f: impl FnOnce(TextInputLayout<'_>) -> R,
804) -> Option<R> {
805    if !renderer.text_input_has_parley_layout(text_input, item_rc) {
806        return None;
807    }
808    with_text_input_layout_impl(
809        renderer.scale_factor(),
810        renderer.window_adapter(),
811        renderer.text_layout_cache(),
812        text_input,
813        item_rc,
814        size,
815        f,
816    )
817}
818
819fn with_text_input_layout_impl<R>(
820    scale_factor: Option<ScaleFactor>,
821    window_adapter: Option<Rc<dyn WindowAdapter>>,
822    cache: Option<&TextLayoutCache>,
823    text_input: Pin<&crate::items::TextInput>,
824    item_rc: &crate::item_tree::ItemRc,
825    size: LogicalSize,
826    f: impl FnOnce(TextInputLayout<'_>) -> R,
827) -> Option<R> {
828    let scale_factor = scale_factor?;
829    let window_adapter = window_adapter?;
830
831    let width = size.width_length();
832    let height = size.height_length();
833    if width.get() <= 0. || height.get() <= 0. {
834        return None;
835    }
836
837    let layout_builder =
838        shaping_builder(text_input, Some(item_rc), text_input.wrap(), scale_factor);
839
840    // `RenderString for TextInput` yields plain text; a styled input doesn't exist.
841    let PlainOrStyledText::Plain(text) = crate::item_rendering::RenderString::text(text_input)
842    else {
843        return None;
844    };
845
846    with_text_layout(
847        cache,
848        Some(item_rc),
849        text_input,
850        &layout_builder,
851        LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
852        window_adapter.window(),
853        |layout| f(TextInputLayout { layout, text: &text }),
854    )
855}
856
857#[cfg(feature = "accessibility-text")]
858mod accessibility;
859#[cfg(feature = "accessibility-text")]
860pub use accessibility::CachedTextInputAccessibilityState;