Skip to main content

inset_embedder/
paragraph.rs

1//! Flutter counterpart: `engine/src/flutter/lib/ui/text.dart`, the paragraph half:
2//! `TextStyle`, `ParagraphStyle`, `ParagraphBuilder`, `Paragraph`, and the position, range,
3//! box, and line-metric value types.
4//!
5//! Flutter's engine implements these on top of Skia's paragraph library; here they wrap valo's.
6//! Offsets are UTF-16 code units, as in Dart; valo works in UTF-8 bytes and the wrapper
7//! translates.
8
9use std::fmt;
10use std::ops::Range;
11
12use crate::fonts::FontCollection;
13use crate::text::{
14    FontFeature, FontStyle, FontVariation, FontWeight, TextAlign, TextBaseline, TextDecoration,
15    TextDecorationStyle, TextDirection, TextHeightBehavior, TextLeadingDistribution,
16};
17use crate::{Canvas, Color, Offset, Paint, Rect, Shadow};
18
19/// A position in a string of text.
20///
21/// A `TextPosition` can be used to describe a caret position in between characters. The
22/// [`offset`](Self::offset) points to the position between `offset - 1` and `offset`
23/// characters of the string, and the [`affinity`](Self::affinity) is used to describe which
24/// character this position affiliates with.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct TextPosition {
27    /// The index of the character that immediately follows the position in the string
28    /// representation of the text.
29    pub offset: i32,
30    /// Disambiguates cases where the offset could be at the end of one line or the
31    /// start of the next.
32    pub affinity: TextAffinity,
33}
34
35impl TextPosition {
36    /// Creates an object representing a particular position in a string, with downstream
37    /// affinity.
38    pub const fn new(offset: i32) -> TextPosition {
39        TextPosition {
40            offset,
41            affinity: TextAffinity::Downstream,
42        }
43    }
44
45    /// Creates an object representing a particular position in a string.
46    pub const fn with_affinity(offset: i32, affinity: TextAffinity) -> TextPosition {
47        TextPosition { offset, affinity }
48    }
49}
50
51/// A way to disambiguate a [`TextPosition`] when its offset could match two
52/// different locations in the rendered string.
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
54pub enum TextAffinity {
55    /// The position has affinity for the upstream side of the text position, i.e. in the
56    /// direction of the beginning of the string.
57    Upstream,
58    /// The position has affinity for the downstream side of the text position, i.e. in the
59    /// direction of the end of the string.
60    #[default]
61    Downstream,
62}
63
64/// A range of characters in a string of text.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66pub struct TextRange {
67    /// The index of the first character in the range.
68    ///
69    /// If [`start`](Self::start) and [`end`](Self::end) are both -1, the text range is empty.
70    pub start: i32,
71    /// The next index after the characters in this range.
72    pub end: i32,
73}
74
75impl TextRange {
76    /// A text range that starts and ends at offset -1: the empty range.
77    pub const EMPTY: TextRange = TextRange { start: -1, end: -1 };
78
79    /// Creates a new [`TextRange`]. The `start` and `end` must be at least -1.
80    pub const fn new(start: i32, end: i32) -> TextRange {
81        debug_assert!(start >= -1);
82        debug_assert!(end >= -1);
83        TextRange { start, end }
84    }
85
86    /// A text range that starts and ends at `offset`.
87    pub const fn collapsed(offset: i32) -> TextRange {
88        debug_assert!(offset >= -1);
89        TextRange {
90            start: offset,
91            end: offset,
92        }
93    }
94
95    /// Whether this range represents a valid position in the text.
96    pub fn is_valid(&self) -> bool {
97        self.start >= 0 && self.end >= 0
98    }
99
100    /// Whether this range is empty (but still potentially placed inside the text).
101    pub fn is_collapsed(&self) -> bool {
102        self.start == self.end
103    }
104
105    /// Whether the start of this range precedes the end.
106    pub fn is_normalized(&self) -> bool {
107        self.end >= self.start
108    }
109
110    /// The text before this range.
111    pub fn text_before<'a>(&self, text: &'a str) -> &'a str {
112        debug_assert!(self.is_normalized());
113        let start = byte_of_code_unit(text, self.start as usize);
114        &text[..start]
115    }
116
117    /// The text after this range.
118    pub fn text_after<'a>(&self, text: &'a str) -> &'a str {
119        debug_assert!(self.is_normalized());
120        let end = byte_of_code_unit(text, self.end as usize);
121        &text[end..]
122    }
123
124    /// The text inside this range.
125    pub fn text_inside<'a>(&self, text: &'a str) -> &'a str {
126        debug_assert!(self.is_normalized());
127        let start = byte_of_code_unit(text, self.start as usize);
128        let end = byte_of_code_unit(text, self.end as usize);
129        &text[start..end]
130    }
131}
132
133/// Layout constraints for [`Paragraph`] objects.
134///
135/// Instances of this class are typically used with [`Paragraph::layout`].
136#[derive(Clone, Copy, Debug, PartialEq)]
137pub struct ParagraphConstraints {
138    /// The width the paragraph should use whey computing the positions of glyphs.
139    ///
140    /// If possible, the paragraph will select a soft line break prior to reaching this width.
141    /// If no soft line break is available, the paragraph will select a hard line break prior
142    /// to reaching this width. If that would force a line break without any glyphs, the
143    /// paragraph will place the first glyph on the line anyway.
144    pub width: f64,
145}
146
147impl ParagraphConstraints {
148    /// Creates constraints for laying out a paragraph.
149    pub const fn new(width: f64) -> ParagraphConstraints {
150        ParagraphConstraints { width }
151    }
152}
153
154/// A rectangle enclosing a run of text.
155///
156/// This is similar to [`Rect`] but includes an inherent [`TextDirection`].
157#[derive(Clone, Copy, Debug, PartialEq)]
158pub struct TextBox {
159    /// The left edge of the text box, relative to the parent widget.
160    pub left: f64,
161    /// The top edge of the text box, relative to the parent widget.
162    pub top: f64,
163    /// The right edge of the text box, relative to the parent widget.
164    pub right: f64,
165    /// The bottom edge of the text box, relative to the parent widget.
166    pub bottom: f64,
167    /// The direction in which text inside this box flows.
168    pub direction: TextDirection,
169}
170
171impl TextBox {
172    /// Creates an object that describes a box containing text.
173    pub const fn from_ltrbd(
174        left: f64,
175        top: f64,
176        right: f64,
177        bottom: f64,
178        direction: TextDirection,
179    ) -> TextBox {
180        TextBox {
181            left,
182            top,
183            right,
184            bottom,
185            direction,
186        }
187    }
188
189    /// Returns a rect of the same size as this box.
190    pub fn to_rect(&self) -> Rect {
191        Rect::from_ltrb(self.left, self.top, self.right, self.bottom)
192    }
193
194    /// The [`left`](Self::left) edge of the box for left-to-right text; the
195    /// [`right`](Self::right) edge of the box for right-to-left text.
196    pub fn start(&self) -> f64 {
197        match self.direction {
198            TextDirection::Ltr => self.left,
199            TextDirection::Rtl => self.right,
200        }
201    }
202
203    /// The [`right`](Self::right) edge of the box for left-to-right text; the
204    /// [`left`](Self::left) edge of the box for right-to-left text.
205    pub fn end(&self) -> f64 {
206        match self.direction {
207            TextDirection::Ltr => self.right,
208            TextDirection::Rtl => self.left,
209        }
210    }
211}
212
213/// Where to vertically align the placeholder relative to the surrounding text.
214#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
215pub enum PlaceholderAlignment {
216    Baseline,
217    AboveBaseline,
218    BelowBaseline,
219    Top,
220    Bottom,
221    Middle,
222}
223
224/// Defines various ways to vertically bound the boxes returned by
225/// [`Paragraph::get_boxes_for_range`].
226#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
227pub enum BoxHeightStyle {
228    #[default]
229    Tight,
230    Max,
231    IncludeLineSpacingMiddle,
232    IncludeLineSpacingTop,
233    IncludeLineSpacingBottom,
234    Strut,
235}
236
237/// Defines various ways to horizontally bound the boxes returned by
238/// [`Paragraph::get_boxes_for_range`].
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
240pub enum BoxWidthStyle {
241    #[default]
242    Tight,
243    Max,
244}
245
246/// The measurements of a character (or a sequence of visually connected characters) within
247/// a paragraph.
248#[derive(Clone, Copy, Debug, PartialEq)]
249pub struct GlyphInfo {
250    /// The layout bounding rect of the associated character, in the paragraph's coordinates.
251    pub grapheme_cluster_layout_bounds: Rect,
252    /// The UTF-16 range of the associated character in the text.
253    pub grapheme_cluster_code_unit_range: TextRange,
254    /// The writing direction within the glyph.
255    pub writing_direction: TextDirection,
256}
257
258/// [`LineMetrics`] stores the measurements and statistics of a single line in the paragraph.
259#[derive(Clone, Copy, Debug, PartialEq)]
260pub struct LineMetrics {
261    /// True if this line ends with an explicit line break (e.g. `\n`) or is the end of the
262    /// paragraph. False otherwise.
263    pub hard_break: bool,
264    /// The rise from the [`baseline`](Self::baseline) as calculated from the font and style
265    /// for this line.
266    pub ascent: f64,
267    /// The drop from the [`baseline`](Self::baseline) as calculated from the font and style
268    /// for this line.
269    pub descent: f64,
270    /// The rise from the [`baseline`](Self::baseline) as calculated from the font and style
271    /// for this line ignoring the `TextStyle.height`.
272    pub unscaled_ascent: f64,
273    /// Total height of the line from the top edge to the bottom edge.
274    pub height: f64,
275    /// Width of the line from the left edge of the leftmost glyph to the right edge of the
276    /// rightmost glyph.
277    pub width: f64,
278    /// The x coordinate of left edge of the line.
279    pub left: f64,
280    /// The y coordinate of the baseline for this line from the top of the paragraph.
281    pub baseline: f64,
282    /// The number of this line in the overall paragraph, with the first line being index zero.
283    pub line_number: i32,
284}
285
286/// An opaque object that determines the size, position, and rendering of text.
287///
288/// Every field is optional, as in Dart: a field left `None` inherits from the style below it
289/// on the [`ParagraphBuilder`]'s stack, or from the paragraph style.
290#[derive(Clone, Debug, Default, PartialEq)]
291pub struct TextStyle {
292    pub color: Option<Color>,
293    pub decoration: Option<TextDecoration>,
294    pub decoration_color: Option<Color>,
295    pub decoration_style: Option<TextDecorationStyle>,
296    pub decoration_thickness: Option<f64>,
297    pub font_weight: Option<FontWeight>,
298    pub font_style: Option<FontStyle>,
299    pub text_baseline: Option<TextBaseline>,
300    pub font_family: Option<String>,
301    pub font_family_fallback: Option<Vec<String>>,
302    pub font_size: Option<f64>,
303    pub letter_spacing: Option<f64>,
304    pub word_spacing: Option<f64>,
305    pub height: Option<f64>,
306    pub leading_distribution: Option<TextLeadingDistribution>,
307    pub background: Option<Paint>,
308    pub foreground: Option<Paint>,
309    pub shadows: Option<Vec<Shadow>>,
310    pub font_features: Option<Vec<FontFeature>>,
311    pub font_variations: Option<Vec<FontVariation>>,
312}
313
314impl TextStyle {
315    /// Creates a new text style with every field unset.
316    pub fn new() -> TextStyle {
317        TextStyle::default()
318    }
319
320    /// The style with `other`'s set fields laid over this one, the engine's `pushStyle`
321    /// inheritance.
322    fn merged_with(&self, other: &TextStyle) -> TextStyle {
323        TextStyle {
324            color: other.color.or(self.color),
325            decoration: other.decoration.or(self.decoration),
326            decoration_color: other.decoration_color.or(self.decoration_color),
327            decoration_style: other.decoration_style.or(self.decoration_style),
328            decoration_thickness: other.decoration_thickness.or(self.decoration_thickness),
329            font_weight: other.font_weight.or(self.font_weight),
330            font_style: other.font_style.or(self.font_style),
331            text_baseline: other.text_baseline.or(self.text_baseline),
332            font_family: other
333                .font_family
334                .clone()
335                .or_else(|| self.font_family.clone()),
336            font_family_fallback: other
337                .font_family_fallback
338                .clone()
339                .or_else(|| self.font_family_fallback.clone()),
340            font_size: other.font_size.or(self.font_size),
341            letter_spacing: other.letter_spacing.or(self.letter_spacing),
342            word_spacing: other.word_spacing.or(self.word_spacing),
343            height: other.height.or(self.height),
344            leading_distribution: other.leading_distribution.or(self.leading_distribution),
345            background: other.background.clone().or_else(|| self.background.clone()),
346            foreground: other.foreground.clone().or_else(|| self.foreground.clone()),
347            shadows: other.shadows.clone().or_else(|| self.shadows.clone()),
348            font_features: other
349                .font_features
350                .clone()
351                .or_else(|| self.font_features.clone()),
352            font_variations: other
353                .font_variations
354                .clone()
355                .or_else(|| self.font_variations.clone()),
356        }
357    }
358
359    /// The host span style: unset fields take the host's defaults (14px, black).
360    fn to_native(&self) -> valo::TextStyle {
361        let mut style = valo::TextStyle::default();
362        let mut families = Vec::new();
363        if let Some(family) = &self.font_family {
364            families.push(family.clone());
365        }
366        if let Some(fallback) = &self.font_family_fallback {
367            families.extend(fallback.iter().cloned());
368        }
369        style.families = families;
370        if let Some(weight) = self.font_weight {
371            style.weight = weight.value as u16;
372        }
373        style.italic = self.font_style == Some(FontStyle::Italic);
374        if let Some(size) = self.font_size {
375            style.size = size as f32;
376        }
377        if let Some(foreground) = &self.foreground {
378            style.color = foreground.color;
379        } else if let Some(color) = self.color {
380            style.color = color.into();
381        }
382        if let Some(letter_spacing) = self.letter_spacing {
383            style.letter_spacing = letter_spacing as f32;
384        }
385        if let Some(word_spacing) = self.word_spacing {
386            style.word_spacing = word_spacing as f32;
387        }
388        style.height = match self.height {
389            Some(height) if height != crate::text::K_TEXT_HEIGHT_NONE => Some(height as f32),
390            _ => None,
391        };
392        style.decoration = native_decoration(
393            self.decoration,
394            self.decoration_color,
395            self.decoration_thickness,
396        );
397        if let Some(shadows) = &self.shadows {
398            style.shadows = shadows
399                .iter()
400                .map(|shadow| valo::Shadow {
401                    color: shadow.color.into(),
402                    offset: shadow.offset.into(),
403                    blur: shadow.blur_sigma() as f32,
404                })
405                .collect();
406        }
407        style
408    }
409}
410
411/// Underline before overline before line-through: valo draws one decoration per run.
412fn native_decoration(
413    decoration: Option<TextDecoration>,
414    color: Option<Color>,
415    thickness: Option<f64>,
416) -> Option<valo::Decoration> {
417    let decoration = decoration?;
418    let kind = if decoration.contains(TextDecoration::UNDERLINE) {
419        valo::DecorationKind::Underline
420    } else if decoration.contains(TextDecoration::OVERLINE) {
421        valo::DecorationKind::Overline
422    } else if decoration.contains(TextDecoration::LINE_THROUGH) {
423        valo::DecorationKind::LineThrough
424    } else {
425        return None;
426    };
427    Some(valo::Decoration {
428        kind,
429        color: color.map(Into::into),
430        thickness: thickness.unwrap_or(1.0) as f32,
431    })
432}
433
434/// An opaque object that determines the configuration used by [`ParagraphBuilder`] to
435/// position lines within a [`Paragraph`] of text.
436///
437/// The font fields (`font_family`, `font_size`, `height`, `font_weight`, `font_style`) are
438/// the style for text that no pushed [`TextStyle`] covers.
439#[derive(Clone, Debug, Default, PartialEq)]
440pub struct ParagraphStyle {
441    pub text_align: Option<TextAlign>,
442    pub text_direction: Option<TextDirection>,
443    pub max_lines: Option<i32>,
444    pub font_family: Option<String>,
445    pub font_size: Option<f64>,
446    pub height: Option<f64>,
447    pub text_height_behavior: Option<TextHeightBehavior>,
448    pub font_weight: Option<FontWeight>,
449    pub font_style: Option<FontStyle>,
450    pub ellipsis: Option<String>,
451}
452
453impl ParagraphStyle {
454    /// Creates a new paragraph style with every field unset.
455    pub fn new() -> ParagraphStyle {
456        ParagraphStyle::default()
457    }
458
459    /// The text style the paragraph style implies for text with no pushed style.
460    fn base_text_style(&self) -> TextStyle {
461        TextStyle {
462            font_family: self.font_family.clone(),
463            font_size: self.font_size,
464            height: self.height,
465            font_weight: self.font_weight,
466            font_style: self.font_style,
467            ..TextStyle::default()
468        }
469    }
470
471    fn to_native(&self) -> valo::ParagraphStyle {
472        valo::ParagraphStyle {
473            align: native_align(self.text_align, self.text_direction),
474            direction: self.text_direction.map(native_direction),
475            preserve_trailing_whitespace: false,
476            max_lines: self.max_lines.map(|max_lines| max_lines as u32),
477            ellipsis: self.ellipsis.clone(),
478        }
479    }
480}
481
482/// valo stores a physical alignment: `Start` / `End` resolve against the direction, LTR when
483/// none is given.
484fn native_align(align: Option<TextAlign>, direction: Option<TextDirection>) -> valo::TextAlign {
485    match align {
486        None | Some(TextAlign::Left) => valo::TextAlign::Left,
487        Some(TextAlign::Right) => valo::TextAlign::Right,
488        Some(TextAlign::Center) => valo::TextAlign::Center,
489        Some(TextAlign::Justify) => valo::TextAlign::Justify,
490        Some(TextAlign::Start) => match direction {
491            Some(TextDirection::Rtl) => valo::TextAlign::Right,
492            _ => valo::TextAlign::Left,
493        },
494        Some(TextAlign::End) => match direction {
495            Some(TextDirection::Rtl) => valo::TextAlign::Left,
496            _ => valo::TextAlign::Right,
497        },
498    }
499}
500
501fn native_direction(direction: TextDirection) -> valo::TextDirection {
502    match direction {
503        TextDirection::Ltr => valo::TextDirection::Ltr,
504        TextDirection::Rtl => valo::TextDirection::Rtl,
505    }
506}
507
508/// Builds a [`Paragraph`] containing text with the given styling information.
509///
510/// To set the paragraph's alignment, truncation, and ellipsizing behavior, pass an
511/// appropriately-configured [`ParagraphStyle`] object to [`ParagraphBuilder::new`].
512///
513/// Then, call combinations of [`push_style`](Self::push_style), [`add_text`](Self::add_text),
514/// and [`pop`](Self::pop) to add styled text to the object.
515///
516/// Finally, call [`build`](Self::build) to obtain the constructed [`Paragraph`] object. After
517/// this point, the builder is no longer usable.
518///
519/// After constructing a [`Paragraph`], call [`Paragraph::layout`] on it and then paint it
520/// with [`CanvasText::draw_paragraph`].
521pub struct ParagraphBuilder {
522    paragraph_style: ParagraphStyle,
523    style_stack: Vec<TextStyle>,
524    runs: Vec<(String, valo::TextStyle)>,
525}
526
527impl ParagraphBuilder {
528    /// Creates a new [`ParagraphBuilder`] object, which is used to create a [`Paragraph`].
529    pub fn new(style: ParagraphStyle) -> ParagraphBuilder {
530        let base = style.base_text_style();
531        ParagraphBuilder {
532            paragraph_style: style,
533            style_stack: vec![base],
534            runs: Vec::new(),
535        }
536    }
537
538    /// The number of placeholders currently in the paragraph.
539    ///
540    /// Placeholders are not supported by the host; this is always zero.
541    pub fn placeholder_count(&self) -> i32 {
542        0
543    }
544
545    /// Applies the given style to the added text until [`pop`](Self::pop) is called.
546    ///
547    /// Fields the style leaves unset inherit from the style below it.
548    pub fn push_style(&mut self, style: TextStyle) {
549        let current = self
550            .style_stack
551            .last()
552            .expect("the base style is never popped");
553        let merged = current.merged_with(&style);
554        self.style_stack.push(merged);
555    }
556
557    /// Ends the effect of the most recent call to [`push_style`](Self::push_style).
558    ///
559    /// Internally, the paragraph builder maintains a stack of text styles. Text added to the
560    /// paragraph is affected by all the styles in the stack. Calling [`pop`](Self::pop)
561    /// removes the topmost style in the stack, leaving the remaining styles in effect.
562    pub fn pop(&mut self) {
563        if self.style_stack.len() > 1 {
564            self.style_stack.pop();
565        }
566    }
567
568    /// Adds the given text to the paragraph.
569    ///
570    /// The text will be styled according to the current stack of text styles.
571    pub fn add_text(&mut self, text: &str) {
572        let style = self
573            .style_stack
574            .last()
575            .expect("the base style is never popped")
576            .to_native();
577        self.runs.push((text.to_owned(), style));
578    }
579
580    /// Applies the given paragraph style and returns a [`Paragraph`] containing the added
581    /// text and associated styling, shaped against `fonts`.
582    ///
583    /// After calling this function, the paragraph builder object is invalid and cannot be
584    /// used further.
585    pub fn build(self, fonts: &mut FontCollection) -> Paragraph {
586        let direction = self
587            .paragraph_style
588            .text_direction
589            .unwrap_or(TextDirection::Ltr);
590        let mut builder = valo::ParagraphBuilder::new(fonts);
591        builder.style(self.paragraph_style.to_native());
592        for (text, style) in &self.runs {
593            builder.add_text(text, style);
594        }
595        let inner = builder.build();
596        Paragraph {
597            code_units: CodeUnits::of(inner.text()),
598            inner,
599            direction,
600            layout_width: 0.0,
601            disposed: false,
602        }
603    }
604}
605
606/// The map between Dart's UTF-16 code unit offsets and valo's UTF-8 byte offsets.
607#[derive(Clone, Debug)]
608struct CodeUnits {
609    /// `byte_of[code_unit]` for every code unit boundary, plus the end.
610    byte_of: Vec<usize>,
611    /// The number of UTF-16 code units.
612    len: usize,
613}
614
615impl CodeUnits {
616    fn of(text: &str) -> CodeUnits {
617        let mut byte_of = Vec::with_capacity(text.len() + 1);
618        for (byte, ch) in text.char_indices() {
619            byte_of.push(byte);
620            if ch.len_utf16() == 2 {
621                // A surrogate pair: the second code unit sits inside the character.
622                byte_of.push(byte);
623            }
624        }
625        byte_of.push(text.len());
626        let len = byte_of.len() - 1;
627        CodeUnits { byte_of, len }
628    }
629
630    fn byte(&self, code_unit: i32) -> usize {
631        let clamped = code_unit.clamp(0, self.len as i32) as usize;
632        self.byte_of[clamped]
633    }
634
635    /// The first code unit at or before `byte` (a surrogate pair's two units share a byte).
636    fn code_unit(&self, byte: usize) -> i32 {
637        let first_at_or_after = self.byte_of.partition_point(|candidate| *candidate < byte);
638        match self.byte_of.get(first_at_or_after) {
639            Some(candidate) if *candidate == byte => first_at_or_after as i32,
640            _ => first_at_or_after.saturating_sub(1) as i32,
641        }
642    }
643}
644
645/// The byte offset of the given UTF-16 code unit in `text`.
646fn byte_of_code_unit(text: &str, code_unit: usize) -> usize {
647    CodeUnits::of(text).byte(code_unit as i32)
648}
649
650/// A paragraph of text.
651///
652/// A paragraph retains the size and position of each glyph in the text and can be
653/// efficiently resized and painted.
654///
655/// To create a [`Paragraph`] object, use a [`ParagraphBuilder`].
656///
657/// Paragraphs can be displayed on a [`Canvas`] using [`CanvasText::draw_paragraph`].
658pub struct Paragraph {
659    inner: valo::Paragraph,
660    code_units: CodeUnits,
661    direction: TextDirection,
662    /// The width given to the last [`layout`](Self::layout); Skia's `getMaxWidth`.
663    layout_width: f64,
664    disposed: bool,
665}
666
667impl Paragraph {
668    /// The amount of horizontal space this paragraph occupies.
669    ///
670    /// Valid only after [`layout`](Self::layout) has been called. As in Skia, this is the
671    /// width the paragraph was laid out at (possibly infinite), not the widest line: the
672    /// lines are aligned within it.
673    pub fn width(&self) -> f64 {
674        self.layout_width
675    }
676
677    /// The amount of vertical space this paragraph occupies.
678    ///
679    /// Valid only after [`layout`](Self::layout) has been called.
680    pub fn height(&self) -> f64 {
681        f64::from(self.inner.height())
682    }
683
684    /// The distance from the left edge of the leftmost glyph to the right edge of the
685    /// rightmost glyph in the paragraph.
686    pub fn longest_line(&self) -> f64 {
687        f64::from(self.inner.longest_line())
688    }
689
690    /// The minimum width that this paragraph could be without failing to paint its
691    /// contents within itself.
692    pub fn min_intrinsic_width(&self) -> f64 {
693        f64::from(self.inner.min_intrinsic_width())
694    }
695
696    /// Returns the smallest width beyond which increasing the width never decreases the
697    /// height.
698    pub fn max_intrinsic_width(&self) -> f64 {
699        f64::from(self.inner.max_intrinsic_width())
700    }
701
702    /// The distance from the top of the paragraph to the alphabetic baseline of the first
703    /// line, in logical pixels.
704    pub fn alphabetic_baseline(&self) -> f64 {
705        self.inner
706            .lines()
707            .first()
708            .map_or(0.0, |line| f64::from(line.baseline))
709    }
710
711    /// The distance from the top of the paragraph to the ideographic baseline of the first
712    /// line, in logical pixels.
713    ///
714    /// The host has no ideographic metrics; this is the bottom of the first line.
715    pub fn ideographic_baseline(&self) -> f64 {
716        self.inner
717            .lines()
718            .first()
719            .map_or(0.0, |line| f64::from(line.baseline + line.descent))
720    }
721
722    /// True if there is more vertical content, but the text was truncated, either because we
723    /// reached `max_lines` lines of text or because the `max_lines` was `None`, `ellipsis`
724    /// was not `None`, and one of the lines exceeded the width constraint.
725    pub fn did_exceed_max_lines(&self) -> bool {
726        self.inner.truncated()
727    }
728
729    /// Computes the size and position of each glyph in the paragraph.
730    ///
731    /// The [`ParagraphConstraints`] control how wide the text is allowed to be.
732    pub fn layout(&mut self, constraints: ParagraphConstraints) {
733        debug_assert!(!self.disposed);
734        self.layout_width = constraints.width;
735        self.inner.layout(constraints.width as f32);
736    }
737
738    /// Returns a list of text boxes that enclose the given text range.
739    ///
740    /// The host returns tight boxes only: `box_height_style` and `box_width_style` do not
741    /// change the result.
742    pub fn get_boxes_for_range(
743        &self,
744        start: i32,
745        end: i32,
746        box_height_style: BoxHeightStyle,
747        box_width_style: BoxWidthStyle,
748    ) -> Vec<TextBox> {
749        let _ = (box_height_style, box_width_style);
750        let range = self.code_units.byte(start)..self.code_units.byte(end);
751        self.inner
752            .rects_for_range(range)
753            .into_iter()
754            .map(|rect| self.text_box(rect))
755            .collect()
756    }
757
758    fn text_box(&self, rect: valo::Rect) -> TextBox {
759        TextBox::from_ltrbd(
760            f64::from(rect.x),
761            f64::from(rect.y),
762            f64::from(rect.x + rect.width),
763            f64::from(rect.y + rect.height),
764            self.direction,
765        )
766    }
767
768    /// Returns a list of text boxes that enclose all placeholders in the paragraph.
769    ///
770    /// The host has no placeholders; this is always empty.
771    pub fn get_boxes_for_placeholders(&self) -> Vec<TextBox> {
772        Vec::new()
773    }
774
775    /// Returns the text position closest to the given offset.
776    pub fn get_position_for_offset(&self, offset: Offset) -> TextPosition {
777        let position = self.inner.glyph_position_at(offset.into());
778        TextPosition::with_affinity(
779            self.code_units.code_unit(position.offset),
780            if position.downstream {
781                TextAffinity::Downstream
782            } else {
783                TextAffinity::Upstream
784            },
785        )
786    }
787
788    /// Returns the [`GlyphInfo`] of the glyph closest to the given `offset` in the paragraph
789    /// coordinate system, or `None` if if the text is empty, or is entirely clipped or
790    /// ellipsized away.
791    pub fn get_closest_glyph_info_for_offset(&self, offset: Offset) -> Option<GlyphInfo> {
792        let position = self.inner.glyph_position_at(offset.into());
793        let anchor = if position.downstream {
794            position.offset
795        } else {
796            position.offset.saturating_sub(1)
797        };
798        self.glyph_info_at_byte(anchor)
799    }
800
801    /// Returns the [`GlyphInfo`] located at the given UTF-16 `code_unit_offset` in the
802    /// paragraph, or `None` if the given `code_unit_offset` is out of the visible lines' range.
803    pub fn get_glyph_info_at(&self, code_unit_offset: i32) -> Option<GlyphInfo> {
804        if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
805            return None;
806        }
807        self.glyph_info_at_byte(self.code_units.byte(code_unit_offset))
808    }
809
810    /// The glyph whose cluster contains `byte`: its layout box spans the glyph's advance
811    /// horizontally and the line vertically.
812    fn glyph_info_at_byte(&self, byte: usize) -> Option<GlyphInfo> {
813        for line in self.inner.lines() {
814            if !line.range.contains(&byte) {
815                continue;
816            }
817            let top = line.baseline - line.ascent;
818            let bottom = line.baseline + line.descent;
819            let cluster_starts = cluster_starts(line);
820            for run in &line.runs {
821                for glyph in &run.glyphs {
822                    let cluster = cluster_range(&cluster_starts, glyph.cluster, line.range.end);
823                    if !cluster.contains(&byte) {
824                        continue;
825                    }
826                    let (left, right) = if glyph.advance >= 0.0 {
827                        (glyph.x, glyph.x + glyph.advance)
828                    } else {
829                        (glyph.x + glyph.advance, glyph.x)
830                    };
831                    return Some(GlyphInfo {
832                        grapheme_cluster_layout_bounds: Rect::from_ltrb(
833                            f64::from(left),
834                            f64::from(top),
835                            f64::from(right),
836                            f64::from(bottom),
837                        ),
838                        grapheme_cluster_code_unit_range: TextRange::new(
839                            self.code_units.code_unit(cluster.start),
840                            self.code_units.code_unit(cluster.end),
841                        ),
842                        writing_direction: if run.rtl {
843                            TextDirection::Rtl
844                        } else {
845                            TextDirection::Ltr
846                        },
847                    });
848                }
849            }
850        }
851        None
852    }
853
854    /// Returns the [`TextRange`] of the word at the given [`TextPosition`].
855    pub fn get_word_boundary(&self, position: TextPosition) -> TextRange {
856        let range = self
857            .inner
858            .word_boundary(self.code_units.byte(position.offset));
859        self.text_range(range)
860    }
861
862    /// Returns the [`TextRange`] of the line at the given [`TextPosition`].
863    ///
864    /// The newline (if any) is not returned as part of the range.
865    pub fn get_line_boundary(&self, position: TextPosition) -> TextRange {
866        let byte = self.code_units.byte(position.offset);
867        let Some(line) = self.line_containing(byte, position.affinity) else {
868            return TextRange::EMPTY;
869        };
870        let text = self.inner.text();
871        let mut end = line.range.end;
872        if end > line.range.start && text.as_bytes().get(end - 1) == Some(&b'\n') {
873            end -= 1;
874        }
875        self.text_range(line.range.start..end)
876    }
877
878    fn line_containing(&self, byte: usize, affinity: TextAffinity) -> Option<&valo::Line> {
879        let lines = self.inner.lines();
880        let index = lines.iter().position(|line| {
881            let starts_next_line = byte == line.range.start && affinity == TextAffinity::Upstream;
882            (line.range.contains(&byte) && !starts_next_line) || byte == line.range.end
883        });
884        index.and_then(|index| lines.get(index)).or_else(|| {
885            let last = lines.last()?;
886            (byte >= last.range.end).then_some(last)
887        })
888    }
889
890    fn text_range(&self, range: Range<usize>) -> TextRange {
891        TextRange::new(
892            self.code_units.code_unit(range.start),
893            self.code_units.code_unit(range.end),
894        )
895    }
896
897    /// Returns the full list of [`LineMetrics`] that describe in detail the various metrics
898    /// of each laid out line.
899    pub fn compute_line_metrics(&self) -> Vec<LineMetrics> {
900        let lines = self.inner.lines();
901        let text = self.inner.text();
902        lines
903            .iter()
904            .enumerate()
905            .map(|(index, line)| {
906                let is_last = index + 1 == lines.len();
907                let ends_with_newline = line.range.end > line.range.start
908                    && text.as_bytes().get(line.range.end - 1) == Some(&b'\n');
909                LineMetrics {
910                    hard_break: is_last || ends_with_newline,
911                    ascent: f64::from(line.ascent),
912                    descent: f64::from(line.descent),
913                    unscaled_ascent: f64::from(line.ascent),
914                    height: f64::from(line.ascent + line.descent),
915                    width: f64::from(line.width),
916                    left: f64::from(line.left),
917                    baseline: f64::from(line.baseline),
918                    line_number: index as i32,
919                }
920            })
921            .collect()
922    }
923
924    /// Returns the [`LineMetrics`] for the line at `line_number`, or `None` if the given
925    /// `line_number` is greater than or equal to [`number_of_lines`](Self::number_of_lines).
926    pub fn get_line_metrics_at(&self, line_number: i32) -> Option<LineMetrics> {
927        if line_number < 0 {
928            return None;
929        }
930        self.compute_line_metrics()
931            .into_iter()
932            .nth(line_number as usize)
933    }
934
935    /// The total number of visible lines in the paragraph.
936    ///
937    /// Returns a non-negative number. If `max_lines` is set, the value of this getter never
938    /// exceeds `max_lines`.
939    pub fn number_of_lines(&self) -> i32 {
940        self.inner.lines().len() as i32
941    }
942
943    /// Returns the line number of the line that contains the code unit that
944    /// `code_unit_offset` points to, or `None` if the `code_unit_offset` is out of range.
945    pub fn get_line_number_at(&self, code_unit_offset: i32) -> Option<i32> {
946        if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
947            return None;
948        }
949        let byte = self.code_units.byte(code_unit_offset);
950        self.inner
951            .lines()
952            .iter()
953            .position(|line| line.range.contains(&byte))
954            .map(|index| index as i32)
955    }
956
957    /// Release the resources used by this object. The object is no longer usable after this
958    /// method is called.
959    pub fn dispose(&mut self) {
960        debug_assert!(!self.disposed);
961        self.disposed = true;
962    }
963
964    /// Whether this reference to the underlying paragraph is disposed.
965    pub fn debug_disposed(&self) -> bool {
966        self.disposed
967    }
968
969    /// The host paragraph, for drawing.
970    pub(crate) fn native(&self) -> &valo::Paragraph {
971        &self.inner
972    }
973}
974
975/// Every cluster start on the line, in text order. A cluster runs from its start to the next
976/// start on the line, whichever run that glyph sits in.
977fn cluster_starts(line: &valo::Line) -> Vec<usize> {
978    let mut starts: Vec<usize> = line
979        .runs
980        .iter()
981        .flat_map(|run| run.glyphs.iter().map(|glyph| glyph.cluster))
982        .collect();
983    starts.sort_unstable();
984    starts.dedup();
985    starts
986}
987
988fn cluster_range(cluster_starts: &[usize], start: usize, line_end: usize) -> Range<usize> {
989    let next = cluster_starts
990        .iter()
991        .find(|candidate| **candidate > start)
992        .copied()
993        .unwrap_or(line_end);
994    start..next.max(start + 1)
995}
996
997impl fmt::Debug for Paragraph {
998    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
999        f.debug_struct("Paragraph")
1000            .field("text", &self.inner.text())
1001            .field("width", &self.width())
1002            .field("height", &self.height())
1003            .finish()
1004    }
1005}
1006
1007/// Paragraph drawing on a [`Canvas`] (Dart's `Canvas.drawParagraph`).
1008pub trait CanvasText {
1009    /// Draws the text in the given [`Paragraph`] into this canvas at the given [`Offset`].
1010    ///
1011    /// The [`Paragraph`] object must have had [`Paragraph::layout`] called on it first.
1012    fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset);
1013}
1014
1015impl CanvasText for Canvas {
1016    fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset) {
1017        valo::DrawParagraphExt::draw_paragraph(self, paragraph.native(), offset);
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024
1025    #[test]
1026    fn code_units_map_surrogate_pairs_to_one_character() {
1027        let units = CodeUnits::of("a😀b");
1028        assert_eq!(units.len, 4);
1029        assert_eq!(units.byte(0), 0);
1030        assert_eq!(units.byte(1), 1);
1031        assert_eq!(
1032            units.byte(2),
1033            1,
1034            "the second half of the pair maps to the same byte"
1035        );
1036        assert_eq!(units.byte(3), 5);
1037        assert_eq!(units.byte(4), 6);
1038        assert_eq!(units.code_unit(5), 3);
1039        assert_eq!(units.code_unit(6), 4);
1040    }
1041
1042    #[test]
1043    fn text_range_slices_by_code_unit() {
1044        let range = TextRange::new(1, 3);
1045        assert_eq!(range.text_inside("a😀b"), "😀");
1046        assert_eq!(range.text_before("a😀b"), "a");
1047        assert_eq!(range.text_after("a😀b"), "b");
1048        assert!(TextRange::EMPTY.is_collapsed());
1049        assert!(!TextRange::EMPTY.is_valid());
1050    }
1051
1052    #[test]
1053    fn text_box_start_and_end_follow_direction() {
1054        let ltr = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Ltr);
1055        let rtl = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Rtl);
1056        assert_eq!(ltr.start(), 1.0);
1057        assert_eq!(ltr.end(), 3.0);
1058        assert_eq!(rtl.start(), 3.0);
1059        assert_eq!(rtl.end(), 1.0);
1060        assert_eq!(ltr.to_rect(), Rect::from_ltrb(1.0, 2.0, 3.0, 4.0));
1061    }
1062
1063    #[test]
1064    fn pushed_styles_inherit_unset_fields() {
1065        let mut builder = ParagraphBuilder::new(ParagraphStyle {
1066            font_size: Some(20.0),
1067            ..ParagraphStyle::default()
1068        });
1069        builder.push_style(TextStyle {
1070            color: Some(Color::from_argb(255, 255, 0, 0)),
1071            ..TextStyle::default()
1072        });
1073        builder.add_text("x");
1074        builder.pop();
1075        builder.add_text("y");
1076        let styles: Vec<&valo::TextStyle> = builder.runs.iter().map(|(_, s)| s).collect();
1077        assert_eq!(styles[0].size, 20.0, "the child keeps the paragraph's size");
1078        assert_eq!(styles[0].color, Color::from_argb(255, 255, 0, 0).into());
1079        assert_eq!(styles[1].size, 20.0);
1080        assert_eq!(
1081            styles[1].color,
1082            valo::Color::BLACK,
1083            "popping restores the base"
1084        );
1085    }
1086
1087    #[test]
1088    fn start_and_end_alignment_resolve_against_direction() {
1089        assert_eq!(
1090            native_align(Some(TextAlign::Start), Some(TextDirection::Rtl)),
1091            valo::TextAlign::Right
1092        );
1093        assert_eq!(
1094            native_align(Some(TextAlign::End), None),
1095            valo::TextAlign::Right,
1096            "no direction reads as LTR"
1097        );
1098    }
1099
1100    #[test]
1101    fn unspecified_font_family_stays_unset_on_the_native_style() {
1102        let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1103        builder.add_text("x");
1104        assert!(
1105            builder.runs[0].1.families.is_empty(),
1106            "dart:ui leaves fontFamily unset; the collection's default manager supplies the face"
1107        );
1108    }
1109}
1110
1111#[cfg(test)]
1112mod layout_tests {
1113    //! Shaping needs real fonts: these shape against the OS fonts.
1114
1115    use super::*;
1116
1117    fn fonts() -> FontCollection {
1118        let mut fonts = FontCollection::new();
1119        fonts.add_source(valo_system_fonts::SystemFonts::load());
1120        fonts
1121    }
1122
1123    fn paragraph(text: &str, style: ParagraphStyle, width: f64) -> Paragraph {
1124        let mut builder = ParagraphBuilder::new(style);
1125        builder.push_style(TextStyle {
1126            font_size: Some(20.0),
1127            ..TextStyle::default()
1128        });
1129        builder.add_text(text);
1130        let mut paragraph = builder.build(&mut fonts());
1131        paragraph.layout(ParagraphConstraints::new(width));
1132        paragraph
1133    }
1134
1135    #[test]
1136    fn a_laid_out_paragraph_has_a_size_and_one_line() {
1137        let paragraph = paragraph("Hello", ParagraphStyle::new(), f64::INFINITY);
1138        assert_eq!(
1139            paragraph.width(),
1140            f64::INFINITY,
1141            "the layout width, as in Skia"
1142        );
1143        assert!(paragraph.longest_line() > 0.0);
1144        assert!(paragraph.height() > 0.0);
1145        assert_eq!(paragraph.number_of_lines(), 1);
1146        assert!(paragraph.alphabetic_baseline() > 0.0);
1147        assert!(paragraph.alphabetic_baseline() < paragraph.height());
1148        assert!(!paragraph.did_exceed_max_lines());
1149        assert_eq!(paragraph.max_intrinsic_width(), paragraph.longest_line());
1150    }
1151
1152    #[test]
1153    fn a_narrow_width_wraps_onto_more_lines() {
1154        let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1155        let narrow = paragraph(
1156            "one two three four",
1157            ParagraphStyle::new(),
1158            wide.longest_line() / 2.0,
1159        );
1160        assert!(narrow.number_of_lines() > 1);
1161        assert!(narrow.height() > wide.height());
1162        let metrics = narrow.compute_line_metrics();
1163        assert_eq!(metrics.len() as i32, narrow.number_of_lines());
1164        assert!(
1165            metrics.last().unwrap().hard_break,
1166            "the last line is a hard break"
1167        );
1168        assert!(!metrics[0].hard_break, "a wrapped line is a soft break");
1169        assert_eq!(metrics[1].line_number, 1);
1170    }
1171
1172    #[test]
1173    fn max_lines_with_an_ellipsis_truncates() {
1174        let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1175        let style = ParagraphStyle {
1176            max_lines: Some(1),
1177            ellipsis: Some("\u{2026}".to_owned()),
1178            ..ParagraphStyle::default()
1179        };
1180        let clipped = paragraph("one two three four", style, wide.longest_line() / 2.0);
1181        assert_eq!(clipped.number_of_lines(), 1);
1182        assert!(clipped.did_exceed_max_lines());
1183    }
1184
1185    #[test]
1186    fn positions_boxes_and_boundaries_use_code_units() {
1187        let paragraph = paragraph("ab cd", ParagraphStyle::new(), f64::INFINITY);
1188        let start = paragraph.get_position_for_offset(Offset::new(0.0, 1.0));
1189        assert_eq!(start.offset, 0);
1190        let end =
1191            paragraph.get_position_for_offset(Offset::new(paragraph.longest_line() + 50.0, 1.0));
1192        assert_eq!(end.offset, 5);
1193
1194        let boxes =
1195            paragraph.get_boxes_for_range(0, 2, BoxHeightStyle::Tight, BoxWidthStyle::Tight);
1196        assert_eq!(boxes.len(), 1);
1197        assert!(boxes[0].right > boxes[0].left);
1198        assert_eq!(boxes[0].direction, TextDirection::Ltr);
1199
1200        assert_eq!(
1201            paragraph.get_word_boundary(TextPosition::new(4)),
1202            TextRange::new(3, 5)
1203        );
1204        assert_eq!(
1205            paragraph.get_line_boundary(TextPosition::new(1)),
1206            TextRange::new(0, 5)
1207        );
1208        assert_eq!(paragraph.get_line_number_at(4), Some(0));
1209        assert_eq!(paragraph.get_line_number_at(5), None);
1210
1211        let glyph = paragraph.get_glyph_info_at(3).expect("the c glyph");
1212        assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(3, 4));
1213        assert!(glyph.grapheme_cluster_layout_bounds.width() > 0.0);
1214        assert!(paragraph.get_glyph_info_at(5).is_none());
1215    }
1216
1217    #[test]
1218    fn a_surrogate_pair_is_one_glyph_of_two_code_units() {
1219        let paragraph = paragraph("a😀b", ParagraphStyle::new(), f64::INFINITY);
1220        let glyph = paragraph.get_glyph_info_at(1).expect("the emoji");
1221        assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(1, 3));
1222        assert_eq!(
1223            paragraph
1224                .get_glyph_info_at(3)
1225                .unwrap()
1226                .grapheme_cluster_code_unit_range,
1227            TextRange::new(3, 4)
1228        );
1229    }
1230
1231    #[test]
1232    fn drawing_records_the_paragraph() {
1233        let paragraph = paragraph("Hi", ParagraphStyle::new(), f64::INFINITY);
1234        let mut canvas = Canvas::new();
1235        canvas.draw_paragraph(&paragraph, Offset::new(10.0, 10.0));
1236        let picture = canvas.build();
1237        assert!(!picture.ops().is_empty());
1238    }
1239
1240    #[test]
1241    fn unspecified_family_at_black_weight_covers_latin() {
1242        let mut fonts = FontCollection::new();
1243        crate::set_default_font_manager(&mut fonts, Box::new(crate::SystemFontSource::platform()));
1244        let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1245        builder.push_style(TextStyle {
1246            font_size: Some(10.2),
1247            font_weight: Some(FontWeight::W900),
1248            ..TextStyle::default()
1249        });
1250        builder.add_text("DEBUG");
1251        let mut paragraph = builder.build(&mut fonts);
1252        paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1253        assert!(paragraph.longest_line() > 0.0);
1254        assert!(
1255            fonts.take_unanswered().is_empty(),
1256            "the engine default family covers Latin at weight 900"
1257        );
1258        for line in paragraph.inner.lines() {
1259            for run in &line.runs {
1260                assert!(
1261                    run.glyphs.iter().all(|glyph| glyph.id != 0),
1262                    "no .notdef tofu"
1263                );
1264            }
1265        }
1266    }
1267}