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        style.foreground = self.foreground.clone();
383        style.background = self.background.clone();
384        if let Some(letter_spacing) = self.letter_spacing {
385            style.letter_spacing = letter_spacing as f32;
386        }
387        if let Some(word_spacing) = self.word_spacing {
388            style.word_spacing = word_spacing as f32;
389        }
390        style.height = match self.height {
391            Some(height) if height != crate::text::K_TEXT_HEIGHT_NONE => Some(height as f32),
392            _ => None,
393        };
394        style.decoration = native_decoration(
395            self.decoration,
396            self.decoration_color,
397            self.decoration_thickness,
398        );
399        if let Some(shadows) = &self.shadows {
400            style.shadows = shadows
401                .iter()
402                .map(|shadow| valo::Shadow {
403                    color: shadow.color.into(),
404                    offset: shadow.offset.into(),
405                    blur: shadow.blur_sigma() as f32,
406                })
407                .collect();
408        }
409        style
410    }
411}
412
413/// Underline before overline before line-through: valo draws one decoration per run.
414fn native_decoration(
415    decoration: Option<TextDecoration>,
416    color: Option<Color>,
417    thickness: Option<f64>,
418) -> Option<valo::Decoration> {
419    let decoration = decoration?;
420    let kind = if decoration.contains(TextDecoration::UNDERLINE) {
421        valo::DecorationKind::Underline
422    } else if decoration.contains(TextDecoration::OVERLINE) {
423        valo::DecorationKind::Overline
424    } else if decoration.contains(TextDecoration::LINE_THROUGH) {
425        valo::DecorationKind::LineThrough
426    } else {
427        return None;
428    };
429    Some(valo::Decoration {
430        kind,
431        color: color.map(Into::into),
432        thickness: thickness.unwrap_or(1.0) as f32,
433    })
434}
435
436/// An opaque object that determines the configuration used by [`ParagraphBuilder`] to
437/// position lines within a [`Paragraph`] of text.
438///
439/// The font fields (`font_family`, `font_size`, `height`, `font_weight`, `font_style`) are
440/// the style for text that no pushed [`TextStyle`] covers.
441#[derive(Clone, Debug, Default, PartialEq)]
442pub struct ParagraphStyle {
443    pub text_align: Option<TextAlign>,
444    pub text_direction: Option<TextDirection>,
445    pub max_lines: Option<i32>,
446    pub font_family: Option<String>,
447    pub font_size: Option<f64>,
448    pub height: Option<f64>,
449    pub text_height_behavior: Option<TextHeightBehavior>,
450    pub font_weight: Option<FontWeight>,
451    pub font_style: Option<FontStyle>,
452    pub ellipsis: Option<String>,
453}
454
455impl ParagraphStyle {
456    /// Creates a new paragraph style with every field unset.
457    pub fn new() -> ParagraphStyle {
458        ParagraphStyle::default()
459    }
460
461    /// The text style the paragraph style implies for text with no pushed style.
462    fn base_text_style(&self) -> TextStyle {
463        TextStyle {
464            font_family: self.font_family.clone(),
465            font_size: self.font_size,
466            height: self.height,
467            font_weight: self.font_weight,
468            font_style: self.font_style,
469            ..TextStyle::default()
470        }
471    }
472
473    fn to_native(&self) -> valo::ParagraphStyle {
474        valo::ParagraphStyle {
475            align: native_align(self.text_align, self.text_direction),
476            direction: self.text_direction.map(native_direction),
477            preserve_trailing_whitespace: false,
478            max_lines: self.max_lines.map(|max_lines| max_lines as u32),
479            ellipsis: self.ellipsis.clone(),
480        }
481    }
482}
483
484/// valo stores a physical alignment: `Start` / `End` resolve against the direction, LTR when
485/// none is given.
486fn native_align(align: Option<TextAlign>, direction: Option<TextDirection>) -> valo::TextAlign {
487    match align {
488        None | Some(TextAlign::Left) => valo::TextAlign::Left,
489        Some(TextAlign::Right) => valo::TextAlign::Right,
490        Some(TextAlign::Center) => valo::TextAlign::Center,
491        Some(TextAlign::Justify) => valo::TextAlign::Justify,
492        Some(TextAlign::Start) => match direction {
493            Some(TextDirection::Rtl) => valo::TextAlign::Right,
494            _ => valo::TextAlign::Left,
495        },
496        Some(TextAlign::End) => match direction {
497            Some(TextDirection::Rtl) => valo::TextAlign::Left,
498            _ => valo::TextAlign::Right,
499        },
500    }
501}
502
503fn native_placeholder_alignment(alignment: PlaceholderAlignment) -> valo::PlaceholderAlignment {
504    match alignment {
505        PlaceholderAlignment::Baseline => valo::PlaceholderAlignment::Baseline,
506        PlaceholderAlignment::AboveBaseline => valo::PlaceholderAlignment::AboveBaseline,
507        PlaceholderAlignment::BelowBaseline => valo::PlaceholderAlignment::BelowBaseline,
508        PlaceholderAlignment::Top => valo::PlaceholderAlignment::Top,
509        PlaceholderAlignment::Bottom => valo::PlaceholderAlignment::Bottom,
510        PlaceholderAlignment::Middle => valo::PlaceholderAlignment::Middle,
511    }
512}
513
514fn native_baseline(baseline: TextBaseline) -> valo::TextBaseline {
515    match baseline {
516        TextBaseline::Alphabetic => valo::TextBaseline::Alphabetic,
517        TextBaseline::Ideographic => valo::TextBaseline::Ideographic,
518    }
519}
520
521fn native_direction(direction: TextDirection) -> valo::TextDirection {
522    match direction {
523        TextDirection::Ltr => valo::TextDirection::Ltr,
524        TextDirection::Rtl => valo::TextDirection::Rtl,
525    }
526}
527
528/// Builds a [`Paragraph`] containing text with the given styling information.
529///
530/// To set the paragraph's alignment, truncation, and ellipsizing behavior, pass an
531/// appropriately-configured [`ParagraphStyle`] object to [`ParagraphBuilder::new`].
532///
533/// Then, call combinations of [`push_style`](Self::push_style), [`add_text`](Self::add_text),
534/// and [`pop`](Self::pop) to add styled text to the object.
535///
536/// Finally, call [`build`](Self::build) to obtain the constructed [`Paragraph`] object. After
537/// this point, the builder is no longer usable.
538///
539/// After constructing a [`Paragraph`], call [`Paragraph::layout`] on it and then paint it
540/// with [`CanvasText::draw_paragraph`].
541pub struct ParagraphBuilder {
542    paragraph_style: ParagraphStyle,
543    style_stack: Vec<TextStyle>,
544    pieces: Vec<Piece>,
545    placeholder_scales: Vec<f64>,
546}
547
548/// One addition to a [`ParagraphBuilder`], kept until [`ParagraphBuilder::build`] has the fonts
549/// to shape it.
550enum Piece {
551    Text(String, valo::TextStyle),
552    Placeholder(valo::PlaceholderStyle, valo::TextStyle),
553}
554
555impl ParagraphBuilder {
556    /// Creates a new [`ParagraphBuilder`] object, which is used to create a [`Paragraph`].
557    pub fn new(style: ParagraphStyle) -> ParagraphBuilder {
558        let base = style.base_text_style();
559        ParagraphBuilder {
560            paragraph_style: style,
561            style_stack: vec![base],
562            pieces: Vec::new(),
563            placeholder_scales: Vec::new(),
564        }
565    }
566
567    /// The number of placeholders currently in the paragraph.
568    pub fn placeholder_count(&self) -> i32 {
569        self.placeholder_scales.len() as i32
570    }
571
572    /// The scales of the placeholders in the paragraph.
573    pub fn placeholder_scales(&self) -> &[f64] {
574        &self.placeholder_scales
575    }
576
577    /// Applies the given style to the added text until [`pop`](Self::pop) is called.
578    ///
579    /// Fields the style leaves unset inherit from the style below it.
580    pub fn push_style(&mut self, style: TextStyle) {
581        let current = self
582            .style_stack
583            .last()
584            .expect("the base style is never popped");
585        let merged = current.merged_with(&style);
586        self.style_stack.push(merged);
587    }
588
589    /// Ends the effect of the most recent call to [`push_style`](Self::push_style).
590    ///
591    /// Internally, the paragraph builder maintains a stack of text styles. Text added to the
592    /// paragraph is affected by all the styles in the stack. Calling [`pop`](Self::pop)
593    /// removes the topmost style in the stack, leaving the remaining styles in effect.
594    pub fn pop(&mut self) {
595        if self.style_stack.len() > 1 {
596            self.style_stack.pop();
597        }
598    }
599
600    /// Adds the given text to the paragraph.
601    ///
602    /// The text will be styled according to the current stack of text styles.
603    pub fn add_text(&mut self, text: &str) {
604        let style = self.current_style();
605        self.pieces.push(Piece::Text(text.to_owned(), style));
606    }
607
608    /// Adds an inline placeholder space to the paragraph.
609    ///
610    /// The paragraph will contain a rectangular space with no text of the dimensions
611    /// specified.
612    ///
613    /// The `width` and `height` parameters specify the size of the placeholder rectangle.
614    ///
615    /// The `alignment` parameter specifies how the placeholder rectangle will be vertically
616    /// aligned with the surrounding text. When [`PlaceholderAlignment::Baseline`],
617    /// [`PlaceholderAlignment::AboveBaseline`], and [`PlaceholderAlignment::BelowBaseline`]
618    /// alignment modes are used, the baseline needs to be set with the `baseline`.
619    /// When using [`PlaceholderAlignment::Baseline`], `baseline_offset` indicates the distance
620    /// of the baseline down from the top of the rectangle. The default `baseline_offset`
621    /// is the `height`.
622    ///
623    /// Examples:
624    ///
625    /// * For a 30x50 placeholder with the bottom edge aligned with the bottom of the text, use:
626    ///   `add_placeholder(30.0, 50.0, PlaceholderAlignment::Bottom, 1.0, None, None)`
627    /// * For a 30x50 placeholder that is vertically centered around the text, use:
628    ///   `add_placeholder(30.0, 50.0, PlaceholderAlignment::Middle, 1.0, None, None)`.
629    /// * For a 30x50 placeholder that sits completely on top of the alphabetic baseline, use:
630    ///   `add_placeholder(30.0, 50.0, PlaceholderAlignment::AboveBaseline, 1.0, None,
631    ///   Some(TextBaseline::Alphabetic))`
632    /// * For a 30x50 placeholder with 40 pixels above and 10 pixels below the alphabetic
633    ///   baseline, use: `add_placeholder(30.0, 50.0, PlaceholderAlignment::Baseline, 1.0,
634    ///   Some(40.0), Some(TextBaseline::Alphabetic))`
635    ///
636    /// Lines are permitted to break around each placeholder.
637    ///
638    /// Decorations will be drawn based on the font defined in the most recently
639    /// pushed [`TextStyle`]. The decorations are drawn as if unicode text were present
640    /// in the placeholder space, and will draw the same regardless of the height and
641    /// alignment of the placeholder. To hide or manually adjust decorations to fit,
642    /// a text style with the desired decoration behavior should be pushed before
643    /// adding a placeholder.
644    ///
645    /// Any decorations drawn through a placeholder will exist on the same canvas/layer
646    /// as the text. This means any content drawn on top of the space reserved by
647    /// the placeholder will be drawn over the decoration, possibly obscuring the
648    /// decoration.
649    ///
650    /// Placeholders are represented by a unicode 0xFFFC "object replacement character"
651    /// in the text buffer. For each placeholder, one object replacement character is
652    /// added on to the text buffer.
653    ///
654    /// The `scale` parameter will scale the `width` and `height` by the specified amount,
655    /// and keep track of the scale. The scales of placeholders added can be accessed
656    /// through [`placeholder_scales`](Self::placeholder_scales). This is primarily used for
657    /// accessibility scaling.
658    pub fn add_placeholder(
659        &mut self,
660        width: f64,
661        height: f64,
662        alignment: PlaceholderAlignment,
663        scale: f64,
664        baseline_offset: Option<f64>,
665        baseline: Option<TextBaseline>,
666    ) {
667        // Require a baseline to be specified if using a baseline-based alignment.
668        debug_assert!(
669            !matches!(
670                alignment,
671                PlaceholderAlignment::AboveBaseline
672                    | PlaceholderAlignment::BelowBaseline
673                    | PlaceholderAlignment::Baseline
674            ) || baseline.is_some()
675        );
676        // Default the baselineOffset to height if null. This will place the placeholder
677        // fully above the baseline, similar to [PlaceholderAlignment.aboveBaseline].
678        let baseline_offset = baseline_offset.unwrap_or(height);
679        let placeholder = valo::PlaceholderStyle {
680            width: (width * scale) as f32,
681            height: (height * scale) as f32,
682            alignment: native_placeholder_alignment(alignment),
683            baseline: native_baseline(baseline.unwrap_or(TextBaseline::Alphabetic)),
684            baseline_offset: (baseline_offset * scale) as f32,
685        };
686        let style = self.current_style();
687        self.pieces.push(Piece::Placeholder(placeholder, style));
688        self.placeholder_scales.push(scale);
689    }
690
691    /// The style on top of the stack, in valo's terms.
692    fn current_style(&self) -> valo::TextStyle {
693        self.style_stack
694            .last()
695            .expect("the base style is never popped")
696            .to_native()
697    }
698
699    /// Applies the given paragraph style and returns a [`Paragraph`] containing the added
700    /// text and associated styling, shaped against `fonts`.
701    ///
702    /// After calling this function, the paragraph builder object is invalid and cannot be
703    /// used further.
704    pub fn build(self, fonts: &mut FontCollection) -> Paragraph {
705        let direction = self
706            .paragraph_style
707            .text_direction
708            .unwrap_or(TextDirection::Ltr);
709        let mut builder = valo::ParagraphBuilder::new(fonts);
710        builder.style(self.paragraph_style.to_native());
711        for piece in &self.pieces {
712            match piece {
713                Piece::Text(text, style) => builder.add_text(text, style),
714                Piece::Placeholder(placeholder, style) => {
715                    builder.add_placeholder(*placeholder, style)
716                }
717            };
718        }
719        let inner = builder.build();
720        Paragraph {
721            code_units: CodeUnits::of(inner.text()),
722            inner,
723            direction,
724            layout_width: 0.0,
725            disposed: false,
726        }
727    }
728}
729
730/// The map between Dart's UTF-16 code unit offsets and valo's UTF-8 byte offsets.
731#[derive(Clone, Debug)]
732struct CodeUnits {
733    /// `byte_of[code_unit]` for every code unit boundary, plus the end.
734    byte_of: Vec<usize>,
735    /// The number of UTF-16 code units.
736    len: usize,
737}
738
739impl CodeUnits {
740    fn of(text: &str) -> CodeUnits {
741        let mut byte_of = Vec::with_capacity(text.len() + 1);
742        for (byte, ch) in text.char_indices() {
743            byte_of.push(byte);
744            if ch.len_utf16() == 2 {
745                // A surrogate pair: the second code unit sits inside the character.
746                byte_of.push(byte);
747            }
748        }
749        byte_of.push(text.len());
750        let len = byte_of.len() - 1;
751        CodeUnits { byte_of, len }
752    }
753
754    fn byte(&self, code_unit: i32) -> usize {
755        let clamped = code_unit.clamp(0, self.len as i32) as usize;
756        self.byte_of[clamped]
757    }
758
759    /// The first code unit at or before `byte` (a surrogate pair's two units share a byte).
760    fn code_unit(&self, byte: usize) -> i32 {
761        let first_at_or_after = self.byte_of.partition_point(|candidate| *candidate < byte);
762        match self.byte_of.get(first_at_or_after) {
763            Some(candidate) if *candidate == byte => first_at_or_after as i32,
764            _ => first_at_or_after.saturating_sub(1) as i32,
765        }
766    }
767}
768
769/// The byte offset of the given UTF-16 code unit in `text`.
770fn byte_of_code_unit(text: &str, code_unit: usize) -> usize {
771    CodeUnits::of(text).byte(code_unit as i32)
772}
773
774/// A paragraph of text.
775///
776/// A paragraph retains the size and position of each glyph in the text and can be
777/// efficiently resized and painted.
778///
779/// To create a [`Paragraph`] object, use a [`ParagraphBuilder`].
780///
781/// Paragraphs can be displayed on a [`Canvas`] using [`CanvasText::draw_paragraph`].
782pub struct Paragraph {
783    inner: valo::Paragraph,
784    code_units: CodeUnits,
785    direction: TextDirection,
786    /// The width given to the last [`layout`](Self::layout); Skia's `getMaxWidth`.
787    layout_width: f64,
788    disposed: bool,
789}
790
791impl Paragraph {
792    /// The amount of horizontal space this paragraph occupies.
793    ///
794    /// Valid only after [`layout`](Self::layout) has been called. As in Skia, this is the
795    /// width the paragraph was laid out at (possibly infinite), not the widest line: the
796    /// lines are aligned within it.
797    pub fn width(&self) -> f64 {
798        self.layout_width
799    }
800
801    /// The amount of vertical space this paragraph occupies.
802    ///
803    /// Valid only after [`layout`](Self::layout) has been called.
804    pub fn height(&self) -> f64 {
805        f64::from(self.inner.height())
806    }
807
808    /// The distance from the left edge of the leftmost glyph to the right edge of the
809    /// rightmost glyph in the paragraph.
810    pub fn longest_line(&self) -> f64 {
811        f64::from(self.inner.longest_line())
812    }
813
814    /// The minimum width that this paragraph could be without failing to paint its
815    /// contents within itself.
816    pub fn min_intrinsic_width(&self) -> f64 {
817        f64::from(self.inner.min_intrinsic_width())
818    }
819
820    /// Returns the smallest width beyond which increasing the width never decreases the
821    /// height.
822    pub fn max_intrinsic_width(&self) -> f64 {
823        f64::from(self.inner.max_intrinsic_width())
824    }
825
826    /// The distance from the top of the paragraph to the alphabetic baseline of the first
827    /// line, in logical pixels.
828    pub fn alphabetic_baseline(&self) -> f64 {
829        self.inner
830            .lines()
831            .first()
832            .map_or(0.0, |line| f64::from(line.baseline))
833    }
834
835    /// The distance from the top of the paragraph to the ideographic baseline of the first
836    /// line, in logical pixels.
837    ///
838    /// The host has no ideographic metrics; this is the bottom of the first line.
839    pub fn ideographic_baseline(&self) -> f64 {
840        self.inner
841            .lines()
842            .first()
843            .map_or(0.0, |line| f64::from(line.baseline + line.descent))
844    }
845
846    /// True if there is more vertical content, but the text was truncated, either because we
847    /// reached `max_lines` lines of text or because the `max_lines` was `None`, `ellipsis`
848    /// was not `None`, and one of the lines exceeded the width constraint.
849    pub fn did_exceed_max_lines(&self) -> bool {
850        self.inner.truncated()
851    }
852
853    /// Computes the size and position of each glyph in the paragraph.
854    ///
855    /// The [`ParagraphConstraints`] control how wide the text is allowed to be.
856    pub fn layout(&mut self, constraints: ParagraphConstraints) {
857        debug_assert!(!self.disposed);
858        self.layout_width = constraints.width;
859        self.inner.layout(constraints.width as f32);
860    }
861
862    /// Returns a list of text boxes that enclose the given text range.
863    ///
864    /// The host returns tight boxes only: `box_height_style` and `box_width_style` do not
865    /// change the result.
866    pub fn get_boxes_for_range(
867        &self,
868        start: i32,
869        end: i32,
870        box_height_style: BoxHeightStyle,
871        box_width_style: BoxWidthStyle,
872    ) -> Vec<TextBox> {
873        let _ = (box_height_style, box_width_style);
874        let range = self.code_units.byte(start)..self.code_units.byte(end);
875        self.inner
876            .rects_for_range(range)
877            .into_iter()
878            .map(|rect| text_box(rect, self.direction))
879            .collect()
880    }
881
882    /// Returns a list of text boxes that enclose all placeholders in the paragraph.
883    ///
884    /// The order of the boxes are in the same order as passed in through
885    /// [`ParagraphBuilder::add_placeholder`].
886    ///
887    /// Coordinates of the [`TextBox`] are relative to the upper-left corner of the paragraph,
888    /// where positive y values indicate down.
889    pub fn get_boxes_for_placeholders(&self) -> Vec<TextBox> {
890        self.inner
891            .placeholder_boxes()
892            .into_iter()
893            .map(|placeholder| {
894                let direction = match placeholder.direction {
895                    valo::TextDirection::Ltr => TextDirection::Ltr,
896                    valo::TextDirection::Rtl => TextDirection::Rtl,
897                };
898                text_box(placeholder.rect, direction)
899            })
900            .collect()
901    }
902
903    /// Returns the text position closest to the given offset.
904    pub fn get_position_for_offset(&self, offset: Offset) -> TextPosition {
905        let position = self.inner.glyph_position_at(offset.into());
906        TextPosition::with_affinity(
907            self.code_units.code_unit(position.offset),
908            if position.downstream {
909                TextAffinity::Downstream
910            } else {
911                TextAffinity::Upstream
912            },
913        )
914    }
915
916    /// Returns the [`GlyphInfo`] of the glyph closest to the given `offset` in the paragraph
917    /// coordinate system, or `None` if if the text is empty, or is entirely clipped or
918    /// ellipsized away.
919    pub fn get_closest_glyph_info_for_offset(&self, offset: Offset) -> Option<GlyphInfo> {
920        let position = self.inner.glyph_position_at(offset.into());
921        let anchor = if position.downstream {
922            position.offset
923        } else {
924            position.offset.saturating_sub(1)
925        };
926        self.glyph_info_at_byte(anchor)
927    }
928
929    /// Returns the [`GlyphInfo`] located at the given UTF-16 `code_unit_offset` in the
930    /// paragraph, or `None` if the given `code_unit_offset` is out of the visible lines' range.
931    pub fn get_glyph_info_at(&self, code_unit_offset: i32) -> Option<GlyphInfo> {
932        if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
933            return None;
934        }
935        self.glyph_info_at_byte(self.code_units.byte(code_unit_offset))
936    }
937
938    /// The glyph whose cluster contains `byte`: its layout box spans the glyph's advance
939    /// horizontally and the line vertically.
940    fn glyph_info_at_byte(&self, byte: usize) -> Option<GlyphInfo> {
941        for line in self.inner.lines() {
942            if !line.range.contains(&byte) {
943                continue;
944            }
945            let top = line.baseline - line.ascent;
946            let bottom = line.baseline + line.descent;
947            let cluster_starts = cluster_starts(line);
948            for run in &line.runs {
949                for glyph in &run.glyphs {
950                    let cluster = cluster_range(&cluster_starts, glyph.cluster, line.range.end);
951                    if !cluster.contains(&byte) {
952                        continue;
953                    }
954                    let (left, right) = if glyph.advance >= 0.0 {
955                        (glyph.x, glyph.x + glyph.advance)
956                    } else {
957                        (glyph.x + glyph.advance, glyph.x)
958                    };
959                    return Some(GlyphInfo {
960                        grapheme_cluster_layout_bounds: Rect::from_ltrb(
961                            f64::from(left),
962                            f64::from(top),
963                            f64::from(right),
964                            f64::from(bottom),
965                        ),
966                        grapheme_cluster_code_unit_range: TextRange::new(
967                            self.code_units.code_unit(cluster.start),
968                            self.code_units.code_unit(cluster.end),
969                        ),
970                        writing_direction: if run.rtl {
971                            TextDirection::Rtl
972                        } else {
973                            TextDirection::Ltr
974                        },
975                    });
976                }
977            }
978        }
979        None
980    }
981
982    /// Returns the [`TextRange`] of the word at the given [`TextPosition`].
983    pub fn get_word_boundary(&self, position: TextPosition) -> TextRange {
984        let range = self
985            .inner
986            .word_boundary(self.code_units.byte(position.offset));
987        self.text_range(range)
988    }
989
990    /// Returns the [`TextRange`] of the line at the given [`TextPosition`].
991    ///
992    /// The newline (if any) is not returned as part of the range.
993    pub fn get_line_boundary(&self, position: TextPosition) -> TextRange {
994        let byte = self.code_units.byte(position.offset);
995        let Some(line) = self.line_containing(byte, position.affinity) else {
996            return TextRange::EMPTY;
997        };
998        let text = self.inner.text();
999        let mut end = line.range.end;
1000        if end > line.range.start && text.as_bytes().get(end - 1) == Some(&b'\n') {
1001            end -= 1;
1002        }
1003        self.text_range(line.range.start..end)
1004    }
1005
1006    fn line_containing(&self, byte: usize, affinity: TextAffinity) -> Option<&valo::Line> {
1007        let lines = self.inner.lines();
1008        let index = lines.iter().position(|line| {
1009            let starts_next_line = byte == line.range.start && affinity == TextAffinity::Upstream;
1010            (line.range.contains(&byte) && !starts_next_line) || byte == line.range.end
1011        });
1012        index.and_then(|index| lines.get(index)).or_else(|| {
1013            let last = lines.last()?;
1014            (byte >= last.range.end).then_some(last)
1015        })
1016    }
1017
1018    fn text_range(&self, range: Range<usize>) -> TextRange {
1019        TextRange::new(
1020            self.code_units.code_unit(range.start),
1021            self.code_units.code_unit(range.end),
1022        )
1023    }
1024
1025    /// Returns the full list of [`LineMetrics`] that describe in detail the various metrics
1026    /// of each laid out line.
1027    pub fn compute_line_metrics(&self) -> Vec<LineMetrics> {
1028        let lines = self.inner.lines();
1029        let text = self.inner.text();
1030        lines
1031            .iter()
1032            .enumerate()
1033            .map(|(index, line)| {
1034                let is_last = index + 1 == lines.len();
1035                let ends_with_newline = line.range.end > line.range.start
1036                    && text.as_bytes().get(line.range.end - 1) == Some(&b'\n');
1037                LineMetrics {
1038                    hard_break: is_last || ends_with_newline,
1039                    ascent: f64::from(line.ascent),
1040                    descent: f64::from(line.descent),
1041                    unscaled_ascent: f64::from(line.ascent),
1042                    height: f64::from(line.ascent + line.descent),
1043                    width: f64::from(line.width),
1044                    left: f64::from(line.left),
1045                    baseline: f64::from(line.baseline),
1046                    line_number: index as i32,
1047                }
1048            })
1049            .collect()
1050    }
1051
1052    /// Returns the [`LineMetrics`] for the line at `line_number`, or `None` if the given
1053    /// `line_number` is greater than or equal to [`number_of_lines`](Self::number_of_lines).
1054    pub fn get_line_metrics_at(&self, line_number: i32) -> Option<LineMetrics> {
1055        if line_number < 0 {
1056            return None;
1057        }
1058        self.compute_line_metrics()
1059            .into_iter()
1060            .nth(line_number as usize)
1061    }
1062
1063    /// The total number of visible lines in the paragraph.
1064    ///
1065    /// Returns a non-negative number. If `max_lines` is set, the value of this getter never
1066    /// exceeds `max_lines`.
1067    pub fn number_of_lines(&self) -> i32 {
1068        self.inner.lines().len() as i32
1069    }
1070
1071    /// Returns the line number of the line that contains the code unit that
1072    /// `code_unit_offset` points to, or `None` if the `code_unit_offset` is out of range.
1073    pub fn get_line_number_at(&self, code_unit_offset: i32) -> Option<i32> {
1074        if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
1075            return None;
1076        }
1077        let byte = self.code_units.byte(code_unit_offset);
1078        self.inner
1079            .lines()
1080            .iter()
1081            .position(|line| line.range.contains(&byte))
1082            .map(|index| index as i32)
1083    }
1084
1085    /// Release the resources used by this object. The object is no longer usable after this
1086    /// method is called.
1087    pub fn dispose(&mut self) {
1088        debug_assert!(!self.disposed);
1089        self.disposed = true;
1090    }
1091
1092    /// Whether this reference to the underlying paragraph is disposed.
1093    pub fn debug_disposed(&self) -> bool {
1094        self.disposed
1095    }
1096
1097    /// The host paragraph, for drawing.
1098    pub(crate) fn native(&self) -> &valo::Paragraph {
1099        &self.inner
1100    }
1101}
1102
1103fn text_box(rect: valo::Rect, direction: TextDirection) -> TextBox {
1104    TextBox::from_ltrbd(
1105        f64::from(rect.x),
1106        f64::from(rect.y),
1107        f64::from(rect.x + rect.width),
1108        f64::from(rect.y + rect.height),
1109        direction,
1110    )
1111}
1112
1113/// Every cluster start on the line, in text order. A cluster runs from its start to the next
1114/// start on the line, whichever run that glyph sits in.
1115fn cluster_starts(line: &valo::Line) -> Vec<usize> {
1116    let mut starts: Vec<usize> = line
1117        .runs
1118        .iter()
1119        .flat_map(|run| run.glyphs.iter().map(|glyph| glyph.cluster))
1120        .collect();
1121    starts.sort_unstable();
1122    starts.dedup();
1123    starts
1124}
1125
1126fn cluster_range(cluster_starts: &[usize], start: usize, line_end: usize) -> Range<usize> {
1127    let next = cluster_starts
1128        .iter()
1129        .find(|candidate| **candidate > start)
1130        .copied()
1131        .unwrap_or(line_end);
1132    start..next.max(start + 1)
1133}
1134
1135impl fmt::Debug for Paragraph {
1136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1137        f.debug_struct("Paragraph")
1138            .field("text", &self.inner.text())
1139            .field("width", &self.width())
1140            .field("height", &self.height())
1141            .finish()
1142    }
1143}
1144
1145/// Paragraph drawing on a [`Canvas`] (Dart's `Canvas.drawParagraph`).
1146pub trait CanvasText {
1147    /// Draws the text in the given [`Paragraph`] into this canvas at the given [`Offset`].
1148    ///
1149    /// The [`Paragraph`] object must have had [`Paragraph::layout`] called on it first.
1150    fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset);
1151}
1152
1153impl CanvasText for Canvas {
1154    fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset) {
1155        valo::DrawParagraphExt::draw_paragraph(self, paragraph.native(), offset);
1156    }
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161    use super::*;
1162
1163    #[test]
1164    fn code_units_map_surrogate_pairs_to_one_character() {
1165        let units = CodeUnits::of("a😀b");
1166        assert_eq!(units.len, 4);
1167        assert_eq!(units.byte(0), 0);
1168        assert_eq!(units.byte(1), 1);
1169        assert_eq!(
1170            units.byte(2),
1171            1,
1172            "the second half of the pair maps to the same byte"
1173        );
1174        assert_eq!(units.byte(3), 5);
1175        assert_eq!(units.byte(4), 6);
1176        assert_eq!(units.code_unit(5), 3);
1177        assert_eq!(units.code_unit(6), 4);
1178    }
1179
1180    /// A span's foreground and background paints reach valo whole, as Flutter's paragraph builder
1181    /// hands its `TextStyle`'s paints to Skia's; the colour stays the foreground's, for a
1182    /// decoration with no colour of its own.
1183    #[test]
1184    fn a_spans_paints_reach_valo() {
1185        let outline = Paint {
1186            color: Color::from_argb(255, 0, 0, 0).into(),
1187            style: valo::PaintStyle::Stroke(valo::Stroke::new(2.0)),
1188            ..Paint::default()
1189        };
1190        let box_behind = Paint::from_color(Color::from_argb(160, 0, 0, 0).into());
1191        let style = TextStyle {
1192            foreground: Some(outline.clone()),
1193            background: Some(box_behind.clone()),
1194            ..TextStyle::default()
1195        };
1196        let native = style.to_native();
1197        assert_eq!(native.foreground, Some(outline.clone()));
1198        assert_eq!(native.background, Some(box_behind));
1199        assert_eq!(native.color, outline.color);
1200    }
1201
1202    #[test]
1203    fn text_range_slices_by_code_unit() {
1204        let range = TextRange::new(1, 3);
1205        assert_eq!(range.text_inside("a😀b"), "😀");
1206        assert_eq!(range.text_before("a😀b"), "a");
1207        assert_eq!(range.text_after("a😀b"), "b");
1208        assert!(TextRange::EMPTY.is_collapsed());
1209        assert!(!TextRange::EMPTY.is_valid());
1210    }
1211
1212    #[test]
1213    fn text_box_start_and_end_follow_direction() {
1214        let ltr = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Ltr);
1215        let rtl = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Rtl);
1216        assert_eq!(ltr.start(), 1.0);
1217        assert_eq!(ltr.end(), 3.0);
1218        assert_eq!(rtl.start(), 3.0);
1219        assert_eq!(rtl.end(), 1.0);
1220        assert_eq!(ltr.to_rect(), Rect::from_ltrb(1.0, 2.0, 3.0, 4.0));
1221    }
1222
1223    fn text_styles(builder: &ParagraphBuilder) -> Vec<&valo::TextStyle> {
1224        builder
1225            .pieces
1226            .iter()
1227            .filter_map(|piece| match piece {
1228                Piece::Text(_, style) => Some(style),
1229                Piece::Placeholder(..) => None,
1230            })
1231            .collect()
1232    }
1233
1234    #[test]
1235    fn placeholders_are_counted_with_their_scales() {
1236        let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1237        builder.add_text("a");
1238        builder.add_placeholder(10.0, 20.0, PlaceholderAlignment::Middle, 2.0, None, None);
1239        builder.add_placeholder(5.0, 5.0, PlaceholderAlignment::Bottom, 1.0, None, None);
1240        assert_eq!(builder.placeholder_count(), 2);
1241        assert_eq!(builder.placeholder_scales(), [2.0, 1.0]);
1242    }
1243
1244    #[test]
1245    fn pushed_styles_inherit_unset_fields() {
1246        let mut builder = ParagraphBuilder::new(ParagraphStyle {
1247            font_size: Some(20.0),
1248            ..ParagraphStyle::default()
1249        });
1250        builder.push_style(TextStyle {
1251            color: Some(Color::from_argb(255, 255, 0, 0)),
1252            ..TextStyle::default()
1253        });
1254        builder.add_text("x");
1255        builder.pop();
1256        builder.add_text("y");
1257        let styles = text_styles(&builder);
1258        assert_eq!(styles[0].size, 20.0, "the child keeps the paragraph's size");
1259        assert_eq!(styles[0].color, Color::from_argb(255, 255, 0, 0).into());
1260        assert_eq!(styles[1].size, 20.0);
1261        assert_eq!(
1262            styles[1].color,
1263            valo::Color::BLACK,
1264            "popping restores the base"
1265        );
1266    }
1267
1268    #[test]
1269    fn start_and_end_alignment_resolve_against_direction() {
1270        assert_eq!(
1271            native_align(Some(TextAlign::Start), Some(TextDirection::Rtl)),
1272            valo::TextAlign::Right
1273        );
1274        assert_eq!(
1275            native_align(Some(TextAlign::End), None),
1276            valo::TextAlign::Right,
1277            "no direction reads as LTR"
1278        );
1279    }
1280
1281    #[test]
1282    fn unspecified_font_family_stays_unset_on_the_native_style() {
1283        let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1284        builder.add_text("x");
1285        assert!(
1286            text_styles(&builder)[0].families.is_empty(),
1287            "dart:ui leaves fontFamily unset; the collection's default manager supplies the face"
1288        );
1289    }
1290}
1291
1292#[cfg(test)]
1293mod layout_tests {
1294    //! Shaping needs real fonts: these shape against the OS fonts.
1295
1296    use super::*;
1297    use crate::Platform;
1298
1299    fn fonts() -> FontCollection {
1300        let mut fonts = FontCollection::new();
1301        fonts.add_source(valo_system_fonts::SystemFonts::load());
1302        fonts
1303    }
1304
1305    fn paragraph(text: &str, style: ParagraphStyle, width: f64) -> Paragraph {
1306        let mut builder = ParagraphBuilder::new(style);
1307        builder.push_style(TextStyle {
1308            font_size: Some(20.0),
1309            ..TextStyle::default()
1310        });
1311        builder.add_text(text);
1312        let mut paragraph = builder.build(&mut fonts());
1313        paragraph.layout(ParagraphConstraints::new(width));
1314        paragraph
1315    }
1316
1317    #[test]
1318    fn a_laid_out_paragraph_has_a_size_and_one_line() {
1319        let paragraph = paragraph("Hello", ParagraphStyle::new(), f64::INFINITY);
1320        assert_eq!(
1321            paragraph.width(),
1322            f64::INFINITY,
1323            "the layout width, as in Skia"
1324        );
1325        assert!(paragraph.longest_line() > 0.0);
1326        assert!(paragraph.height() > 0.0);
1327        assert_eq!(paragraph.number_of_lines(), 1);
1328        assert!(paragraph.alphabetic_baseline() > 0.0);
1329        assert!(paragraph.alphabetic_baseline() < paragraph.height());
1330        assert!(!paragraph.did_exceed_max_lines());
1331        assert_eq!(paragraph.max_intrinsic_width(), paragraph.longest_line());
1332    }
1333
1334    #[test]
1335    fn a_narrow_width_wraps_onto_more_lines() {
1336        let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1337        let narrow = paragraph(
1338            "one two three four",
1339            ParagraphStyle::new(),
1340            wide.longest_line() / 2.0,
1341        );
1342        assert!(narrow.number_of_lines() > 1);
1343        assert!(narrow.height() > wide.height());
1344        let metrics = narrow.compute_line_metrics();
1345        assert_eq!(metrics.len() as i32, narrow.number_of_lines());
1346        assert!(
1347            metrics.last().unwrap().hard_break,
1348            "the last line is a hard break"
1349        );
1350        assert!(!metrics[0].hard_break, "a wrapped line is a soft break");
1351        assert_eq!(metrics[1].line_number, 1);
1352    }
1353
1354    #[test]
1355    fn max_lines_with_an_ellipsis_truncates() {
1356        let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1357        let style = ParagraphStyle {
1358            max_lines: Some(1),
1359            ellipsis: Some("\u{2026}".to_owned()),
1360            ..ParagraphStyle::default()
1361        };
1362        let clipped = paragraph("one two three four", style, wide.longest_line() / 2.0);
1363        assert_eq!(clipped.number_of_lines(), 1);
1364        assert!(clipped.did_exceed_max_lines());
1365    }
1366
1367    #[test]
1368    fn positions_boxes_and_boundaries_use_code_units() {
1369        let paragraph = paragraph("ab cd", ParagraphStyle::new(), f64::INFINITY);
1370        let start = paragraph.get_position_for_offset(Offset::new(0.0, 1.0));
1371        assert_eq!(start.offset, 0);
1372        let end =
1373            paragraph.get_position_for_offset(Offset::new(paragraph.longest_line() + 50.0, 1.0));
1374        assert_eq!(end.offset, 5);
1375
1376        let boxes =
1377            paragraph.get_boxes_for_range(0, 2, BoxHeightStyle::Tight, BoxWidthStyle::Tight);
1378        assert_eq!(boxes.len(), 1);
1379        assert!(boxes[0].right > boxes[0].left);
1380        assert_eq!(boxes[0].direction, TextDirection::Ltr);
1381
1382        assert_eq!(
1383            paragraph.get_word_boundary(TextPosition::new(4)),
1384            TextRange::new(3, 5)
1385        );
1386        assert_eq!(
1387            paragraph.get_line_boundary(TextPosition::new(1)),
1388            TextRange::new(0, 5)
1389        );
1390        assert_eq!(paragraph.get_line_number_at(4), Some(0));
1391        assert_eq!(paragraph.get_line_number_at(5), None);
1392
1393        let glyph = paragraph.get_glyph_info_at(3).expect("the c glyph");
1394        assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(3, 4));
1395        assert!(glyph.grapheme_cluster_layout_bounds.width() > 0.0);
1396        assert!(paragraph.get_glyph_info_at(5).is_none());
1397    }
1398
1399    #[test]
1400    fn a_surrogate_pair_is_one_glyph_of_two_code_units() {
1401        let paragraph = paragraph("a😀b", ParagraphStyle::new(), f64::INFINITY);
1402        let glyph = paragraph.get_glyph_info_at(1).expect("the emoji");
1403        assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(1, 3));
1404        assert_eq!(
1405            paragraph
1406                .get_glyph_info_at(3)
1407                .unwrap()
1408                .grapheme_cluster_code_unit_range,
1409            TextRange::new(3, 4)
1410        );
1411    }
1412
1413    /// Either half of a glyph finds that glyph, and a point past the end of the line finds the
1414    /// last glyph.
1415    #[test]
1416    fn the_closest_glyph_is_the_one_under_the_point() {
1417        let paragraph = paragraph("abc", ParagraphStyle::new(), f64::INFINITY);
1418        let b = paragraph.get_glyph_info_at(1).expect("the b glyph");
1419        let bounds = b.grapheme_cluster_layout_bounds;
1420        let y = bounds.center().dy();
1421        for x in [bounds.left + 0.5, bounds.right - 0.5] {
1422            let closest = paragraph
1423                .get_closest_glyph_info_for_offset(Offset::new(x, y))
1424                .expect("a glyph");
1425            assert_eq!(
1426                closest.grapheme_cluster_code_unit_range,
1427                TextRange::new(1, 2)
1428            );
1429        }
1430        let past_the_end = paragraph
1431            .get_closest_glyph_info_for_offset(Offset::new(1000.0, y))
1432            .expect("a glyph");
1433        assert_eq!(
1434            past_the_end.grapheme_cluster_code_unit_range,
1435            TextRange::new(2, 3)
1436        );
1437    }
1438
1439    #[test]
1440    fn placeholder_boxes_come_in_the_order_added_at_their_scaled_size() {
1441        let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1442        builder.push_style(TextStyle {
1443            font_size: Some(20.0),
1444            ..TextStyle::default()
1445        });
1446        builder.add_text("ab");
1447        builder.add_placeholder(
1448            30.0,
1449            50.0,
1450            PlaceholderAlignment::Baseline,
1451            1.0,
1452            Some(40.0),
1453            Some(TextBaseline::Alphabetic),
1454        );
1455        builder.add_text("cd");
1456        builder.add_placeholder(10.0, 10.0, PlaceholderAlignment::Bottom, 2.0, None, None);
1457        let mut paragraph = builder.build(&mut fonts());
1458        paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1459
1460        let boxes = paragraph.get_boxes_for_placeholders();
1461        assert_eq!(boxes.len(), 2);
1462        let baseline = paragraph.alphabetic_baseline();
1463        let (first, second) = (boxes[0], boxes[1]);
1464        assert!((first.right - first.left - 30.0).abs() < 1e-3);
1465        assert!(
1466            (first.top - (baseline - 40.0)).abs() < 1e-3,
1467            "40 above the baseline"
1468        );
1469        assert!(
1470            (first.bottom - (baseline + 10.0)).abs() < 1e-3,
1471            "10 below it"
1472        );
1473        assert!(
1474            (second.right - second.left - 20.0).abs() < 1e-3,
1475            "scaled by 2"
1476        );
1477        assert!((second.bottom - second.top - 20.0).abs() < 1e-3);
1478        assert!(second.left > first.right, "after the text between them");
1479        assert_eq!(
1480            paragraph.get_line_boundary(TextPosition::new(0)),
1481            TextRange::new(0, 6),
1482            "each placeholder is one code unit"
1483        );
1484    }
1485
1486    #[test]
1487    fn drawing_records_the_paragraph() {
1488        let paragraph = paragraph("Hi", ParagraphStyle::new(), f64::INFINITY);
1489        let mut canvas = Canvas::new();
1490        canvas.draw_paragraph(&paragraph, Offset::new(10.0, 10.0));
1491        let picture = canvas.build();
1492        assert!(!picture.ops().is_empty());
1493    }
1494
1495    #[test]
1496    fn unspecified_family_at_black_weight_covers_latin() {
1497        let mut fonts = FontCollection::new();
1498        crate::set_default_font_manager(
1499            &mut fonts,
1500            Box::new(crate::SystemFontSource::platform()),
1501            &crate::InertPlatform.default_font_families(),
1502        );
1503        let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1504        builder.push_style(TextStyle {
1505            font_size: Some(10.2),
1506            font_weight: Some(FontWeight::W900),
1507            ..TextStyle::default()
1508        });
1509        builder.add_text("DEBUG");
1510        let mut paragraph = builder.build(&mut fonts);
1511        paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1512        assert!(paragraph.longest_line() > 0.0);
1513        assert!(
1514            fonts.take_unanswered().is_empty(),
1515            "the engine default family covers Latin at weight 900"
1516        );
1517        for line in paragraph.inner.lines() {
1518            for run in &line.runs {
1519                assert!(
1520                    run.glyphs.iter().all(|glyph| glyph.id != 0),
1521                    "no .notdef tofu"
1522                );
1523            }
1524        }
1525    }
1526}