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