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