skia_safe/modules/paragraph/
paragraph.rs

1use std::{ffi, fmt, ops::Range};
2
3use skia_bindings as sb;
4
5use super::{
6    LineMetrics, PositionWithAffinity, RectHeightStyle, RectWidthStyle, TextBox, TextDirection,
7    TextIndex, TextRange,
8};
9use crate::{
10    interop::{Sink, VecSink},
11    prelude::*,
12    scalar, Canvas, Font, GlyphId, Path, Point, Rect, Size, TextBlob, Unichar,
13};
14
15pub type Paragraph = RefHandle<sb::skia_textlayout_Paragraph>;
16// <https://github.com/rust-skia/rust-skia/issues/537>
17// unsafe_send_sync!(Paragraph);
18
19impl NativeDrop for sb::skia_textlayout_Paragraph {
20    fn drop(&mut self) {
21        unsafe { sb::C_Paragraph_delete(self) }
22    }
23}
24
25impl fmt::Debug for Paragraph {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.debug_struct("Paragraph")
28            .field("max_width", &self.max_width())
29            .field("height", &self.height())
30            .field("min_intrinsic_width", &self.min_intrinsic_width())
31            .field("max_intrinsic_width", &self.max_intrinsic_width())
32            .field("alphabetic_baseline", &self.alphabetic_baseline())
33            .field("ideographic_baseline", &self.ideographic_baseline())
34            .field("longest_line", &self.longest_line())
35            .field("did_exceed_max_lines", &self.did_exceed_max_lines())
36            .field("line_number", &self.line_number())
37            .finish()
38    }
39}
40
41impl Paragraph {
42    pub fn max_width(&self) -> scalar {
43        self.native().fWidth
44    }
45
46    pub fn height(&self) -> scalar {
47        self.native().fHeight
48    }
49
50    pub fn min_intrinsic_width(&self) -> scalar {
51        self.native().fMinIntrinsicWidth
52    }
53
54    pub fn max_intrinsic_width(&self) -> scalar {
55        self.native().fMaxIntrinsicWidth
56    }
57
58    pub fn alphabetic_baseline(&self) -> scalar {
59        self.native().fAlphabeticBaseline
60    }
61
62    pub fn ideographic_baseline(&self) -> scalar {
63        self.native().fIdeographicBaseline
64    }
65
66    pub fn longest_line(&self) -> scalar {
67        self.native().fLongestLine
68    }
69
70    pub fn did_exceed_max_lines(&self) -> bool {
71        self.native().fExceededMaxLines
72    }
73
74    pub fn layout(&mut self, width: scalar) {
75        unsafe { sb::C_Paragraph_layout(self.native_mut(), width) }
76    }
77
78    pub fn paint(&self, canvas: &Canvas, p: impl Into<Point>) {
79        let p = p.into();
80        unsafe { sb::C_Paragraph_paint(self.native_mut_force(), canvas.native_mut(), p.x, p.y) }
81    }
82
83    /// Returns a vector of bounding boxes that enclose all text between
84    /// start and end glyph indexes, including start and excluding end
85    pub fn get_rects_for_range(
86        &self,
87        range: Range<usize>,
88        rect_height_style: RectHeightStyle,
89        rect_width_style: RectWidthStyle,
90    ) -> Vec<TextBox> {
91        let mut result: Vec<TextBox> = Vec::new();
92
93        let mut set_tb = |tbs: &[sb::skia_textlayout_TextBox]| {
94            result = tbs.iter().map(TextBox::from_native_ref).cloned().collect();
95        };
96
97        unsafe {
98            sb::C_Paragraph_getRectsForRange(
99                self.native_mut_force(),
100                range.start.try_into().unwrap(),
101                range.end.try_into().unwrap(),
102                rect_height_style.into_native(),
103                rect_width_style.into_native(),
104                VecSink::new(&mut set_tb).native_mut(),
105            );
106        }
107        result
108    }
109
110    pub fn get_rects_for_placeholders(&self) -> Vec<TextBox> {
111        let mut result = Vec::new();
112
113        let mut set_tb = |tbs: &[sb::skia_textlayout_TextBox]| {
114            result = tbs.iter().map(TextBox::from_native_ref).cloned().collect();
115        };
116
117        unsafe {
118            sb::C_Paragraph_getRectsForPlaceholders(
119                self.native_mut_force(),
120                VecSink::new(&mut set_tb).native_mut(),
121            )
122        }
123        result
124    }
125
126    /// Returns the index of the glyph that corresponds to the provided coordinate,
127    /// with the top left corner as the origin, and +y direction as down
128    pub fn get_glyph_position_at_coordinate(&self, p: impl Into<Point>) -> PositionWithAffinity {
129        let p = p.into();
130        let mut r = Default::default();
131        unsafe {
132            sb::C_Paragraph_getGlyphPositionAtCoordinate(self.native_mut_force(), p.x, p.y, &mut r)
133        }
134        r
135    }
136
137    /// Finds the first and last glyphs that define a word containing
138    /// the glyph at index offset
139    pub fn get_word_boundary(&self, offset: u32) -> Range<usize> {
140        let mut range: [usize; 2] = Default::default();
141        unsafe {
142            sb::C_Paragraph_getWordBoundary(self.native_mut_force(), offset, range.as_mut_ptr())
143        }
144        range[0]..range[1]
145    }
146
147    pub fn get_line_metrics(&self) -> Vec<LineMetrics> {
148        let mut result: Vec<LineMetrics> = Vec::new();
149        let mut set_lm = |lms: &[sb::skia_textlayout_LineMetrics]| {
150            result = lms.iter().map(LineMetrics::from_native_ref).collect();
151        };
152
153        unsafe {
154            sb::C_Paragraph_getLineMetrics(
155                self.native_mut_force(),
156                VecSink::new(&mut set_lm).native_mut(),
157            )
158        }
159
160        result
161    }
162
163    pub fn line_number(&self) -> usize {
164        unsafe { sb::C_Paragraph_lineNumber(self.native_mut_force()) }
165    }
166
167    pub fn mark_dirty(&mut self) {
168        unsafe { sb::C_Paragraph_markDirty(self.native_mut()) }
169    }
170
171    /// This function will return the number of unresolved glyphs or
172    /// `None` if not applicable (has not been shaped yet - valid case)
173    pub fn unresolved_glyphs(&mut self) -> Option<usize> {
174        unsafe { sb::C_Paragraph_unresolvedGlyphs(self.native_mut()) }
175            .try_into()
176            .ok()
177    }
178
179    pub fn unresolved_codepoints(&mut self) -> Vec<Unichar> {
180        let mut result = Vec::new();
181
182        let mut set_chars = |chars: &[Unichar]| {
183            result = chars.to_vec();
184        };
185
186        unsafe {
187            sb::C_Paragraph_unresolvedCodepoints(
188                self.native_mut_force(),
189                VecSink::new(&mut set_chars).native_mut(),
190            )
191        }
192
193        result
194    }
195
196    pub fn visit<'a, F>(&mut self, mut visitor: F)
197    where
198        F: FnMut(usize, Option<&'a VisitorInfo>),
199    {
200        unsafe {
201            sb::C_Paragraph_visit(
202                self.native_mut(),
203                &mut visitor as *mut F as *mut _,
204                Some(visitor_trampoline::<'a, F>),
205            );
206        }
207
208        unsafe extern "C" fn visitor_trampoline<'a, F: FnMut(usize, Option<&'a VisitorInfo>)>(
209            ctx: *mut ffi::c_void,
210            index: usize,
211            info: *const sb::skia_textlayout_Paragraph_VisitorInfo,
212        ) {
213            let info = if info.is_null() {
214                None
215            } else {
216                Some(VisitorInfo::from_native_ref(&*info))
217            };
218            (*(ctx as *mut F))(index, info)
219        }
220    }
221
222    pub fn extended_visit<'a, F>(&mut self, mut visitor: F)
223    where
224        F: FnMut(usize, Option<&'a ExtendedVisitorInfo>),
225    {
226        unsafe {
227            sb::C_Paragraph_extendedVisit(
228                self.native_mut(),
229                &mut visitor as *mut F as *mut _,
230                Some(visitor_trampoline::<'a, F>),
231            );
232        }
233
234        unsafe extern "C" fn visitor_trampoline<
235            'a,
236            F: FnMut(usize, Option<&'a ExtendedVisitorInfo>),
237        >(
238            ctx: *mut ffi::c_void,
239            index: usize,
240            info: *const sb::skia_textlayout_Paragraph_ExtendedVisitorInfo,
241        ) {
242            let info = if info.is_null() {
243                None
244            } else {
245                Some(ExtendedVisitorInfo::from_native_ref(&*info))
246            };
247            (*(ctx as *mut F))(index, info)
248        }
249    }
250
251    /// Returns path for a given line
252    ///
253    ///  * `line_number` - a line number
254    ///  * `dest` - a resulting path
255    ///  
256    /// Returns: a number glyphs that could not be converted to path
257    pub fn get_path_at(&mut self, line_number: usize) -> (usize, Path) {
258        let mut path = Path::default();
259        let unconverted_glyphs = unsafe {
260            sb::C_Paragraph_getPath(
261                self.native_mut(),
262                line_number.try_into().unwrap(),
263                path.native_mut(),
264            )
265        };
266        (unconverted_glyphs.try_into().unwrap(), path)
267    }
268
269    /// Returns path for a text blob
270    ///
271    /// * `text_blob` - a text blob
272    ///
273    /// Returns: a path
274    pub fn get_path(text_blob: &mut TextBlob) -> Path {
275        Path::construct(|p| unsafe { sb::C_Paragraph_GetPath(text_blob.native_mut(), p) })
276    }
277
278    /// Checks if a given text blob contains
279    /// glyph with emoji
280    ///
281    /// * `text_blob` - a text blob
282    ///
283    /// Returns: `true` if there is such a glyph
284    pub fn contains_emoji(&mut self, text_blob: &mut TextBlob) -> bool {
285        unsafe { sb::C_Paragraph_containsEmoji(self.native_mut(), text_blob.native_mut()) }
286    }
287
288    /// Checks if a given text blob contains colored font or bitmap
289    ///
290    /// * `text_blob` - a text blob
291    ///
292    /// Returns: `true` if there is such a glyph
293    pub fn contains_color_font_or_bitmap(&mut self, text_blob: &mut TextBlob) -> bool {
294        unsafe {
295            sb::C_Paragraph_containsColorFontOrBitmap(self.native_mut(), text_blob.native_mut())
296        }
297    }
298
299    /// Finds the line number of the line that contains the given UTF-8 index.
300    ///
301    ///  * `index` - a UTF-8 TextIndex into the paragraph
302    ///
303    ///  Returns: the line number the glyph that corresponds to the
304    ///           given `code_unit_index` is in, or -1 if the `code_unit_index`
305    ///           is out of bounds, or when the glyph is truncated or
306    ///           ellipsized away.
307    pub fn get_line_number_at(&self, code_unit_index: TextIndex) -> Option<usize> {
308        // Returns -1 if `code_unit_index` is out of range.
309        unsafe { sb::C_Paragraph_getLineNumberAt(self.native(), code_unit_index) }
310            .try_into()
311            .ok()
312    }
313
314    /// Finds the line number of the line that contains the given UTF-16 index.
315    ///
316    /// * `index` - a UTF-16 offset into the paragraph
317    ///
318    /// Returns: the line number the glyph that corresponds to the
319    ///          given `code_unit_index` is in, or -1 if the `code_unit_index`
320    ///          is out of bounds, or when the glyph is truncated or
321    ///          ellipsized away.
322    pub fn get_line_number_at_utf16_offset(&self, code_unit_index: TextIndex) -> Option<usize> {
323        // Returns -1 if `code_unit_index` is out of range.
324        unsafe {
325            sb::C_Paragraph_getLineNumberAtUTF16Offset(self.native_mut_force(), code_unit_index)
326        }
327        .try_into()
328        .ok()
329    }
330
331    /// Returns line metrics info for the line
332    ///
333    /// * `line_number` - a line number
334    /// * `line_metrics` - an address to return the info (in case of null just skipped)
335    ///
336    /// Returns: `true` if the line is found; `false` if not
337    pub fn get_line_metrics_at(&self, line_number: usize) -> Option<LineMetrics> {
338        let mut r = None;
339        let mut set_lm = |lm: &sb::skia_textlayout_LineMetrics| {
340            r = Some(LineMetrics::from_native_ref(lm));
341        };
342        unsafe {
343            sb::C_Paragraph_getLineMetricsAt(
344                self.native(),
345                line_number,
346                Sink::new(&mut set_lm).native_mut(),
347            )
348        }
349        r
350    }
351
352    /// Returns the visible text on the line (excluding a possible ellipsis)
353    ///
354    /// * `line_number` - a line number
355    /// * `include_spaces` - indicates if the whitespaces should be included
356    ///
357    /// Returns: the range of the text that is shown in the line
358    pub fn get_actual_text_range(&self, line_number: usize, include_spaces: bool) -> TextRange {
359        let mut range = [0usize; 2];
360        unsafe {
361            sb::C_Paragraph_getActualTextRange(
362                self.native(),
363                line_number,
364                include_spaces,
365                range.as_mut_ptr(),
366            )
367        }
368        TextRange {
369            start: range[0],
370            end: range[1],
371        }
372    }
373
374    /// Finds a glyph cluster for text index
375    ///
376    /// * `code_unit_index` - a text index
377    /// * `glyph_info` - a glyph cluster info filled if not null
378    ///
379    /// Returns: `true` if glyph cluster was found; `false` if not
380    pub fn get_glyph_cluster_at(&self, code_unit_index: TextIndex) -> Option<GlyphClusterInfo> {
381        let mut r = None;
382        let mut set_fn = |gci: &sb::skia_textlayout_Paragraph_GlyphClusterInfo| {
383            r = Some(GlyphClusterInfo::from_native_ref(gci))
384        };
385        unsafe {
386            sb::C_Paragraph_getGlyphClusterAt(
387                self.native(),
388                code_unit_index,
389                Sink::new(&mut set_fn).native_mut(),
390            )
391        }
392        r
393    }
394
395    /// Finds the closest glyph cluster for a visual text position
396    ///
397    /// * `dx` - x coordinate
398    /// * `dy` - y coordinate
399    /// * `glyph_info` - a glyph cluster info filled if not null
400    ///
401    /// Returns: `true` if glyph cluster was found; `false` if not
402    ///          (which usually means the paragraph is empty)
403    pub fn get_closest_glyph_cluster_at(&self, d: impl Into<Point>) -> Option<GlyphClusterInfo> {
404        let mut r = None;
405        let mut set_fn = |gci: &sb::skia_textlayout_Paragraph_GlyphClusterInfo| {
406            r = Some(GlyphClusterInfo::from_native_ref(gci))
407        };
408        let d = d.into();
409        unsafe {
410            sb::C_Paragraph_getClosestGlyphClusterAt(
411                self.native(),
412                d.x,
413                d.y,
414                Sink::new(&mut set_fn).native_mut(),
415            )
416        }
417        r
418    }
419
420    /// Retrieves the information associated with the glyph located at the given
421    ///  `code_unit_index`.
422    ///
423    /// * `code_unit_index` - a UTF-16 offset into the paragraph
424    /// * `glyph_info` - an optional GlyphInfo struct to hold the
425    ///                  information associated with the glyph found at the
426    ///                  given index
427    ///
428    /// Returns: `false` only if the offset is out of bounds
429    pub fn get_glyph_info_at_utf16_offset(&mut self, code_unit_index: usize) -> Option<GlyphInfo> {
430        GlyphInfo::try_construct(|gi| unsafe {
431            sb::C_Paragraph_getGlyphInfoAtUTF16Offset(self.native_mut(), code_unit_index, gi)
432        })
433    }
434
435    /// Finds the information associated with the closest glyph to the given
436    /// paragraph coordinates.
437    ///
438    /// * `d` - x/y coordinate
439    /// * `glyph_info` - an optional GlyphInfo struct to hold the
440    ///                  information associated with the glyph found. The
441    ///                  text indices and text ranges are described using
442    ///                   UTF-16 offsets
443    ///
444    /// Returns: `true` if a grapheme cluster was found; `false` if not
445    ///          (which usually means the paragraph is empty)
446    pub fn get_closest_utf16_glyph_info_at(&mut self, d: impl Into<Point>) -> Option<GlyphInfo> {
447        let d = d.into();
448        GlyphInfo::try_construct(|gi| unsafe {
449            sb::C_Paragraph_getClosestUTF16GlyphInfoAt(self.native_mut(), d.x, d.y, gi)
450        })
451    }
452
453    /// Returns the font that is used to shape the text at the position
454    ///
455    /// * `code_unit_index` - text index
456    ///
457    /// Returns: font info or an empty font info if the text is not found
458    pub fn get_font_at(&self, code_unit_index: TextIndex) -> Font {
459        Font::construct(|f| unsafe { sb::C_Paragraph_getFontAt(self.native(), code_unit_index, f) })
460    }
461
462    /// Returns the font used to shape the text at the given UTF-16 offset.
463    ///
464    /// * `code_unit_index` - a UTF-16 offset in the paragraph
465    ///
466    /// Returns: font info or an empty font info if the text is not found
467    pub fn get_font_at_utf16_offset(&mut self, code_unit_index: usize) -> Font {
468        Font::construct(|f| unsafe {
469            sb::C_Paragraph_getFontAtUTF16Offset(self.native_mut(), code_unit_index, f)
470        })
471    }
472
473    /// Returns the information about all the fonts used to shape the paragraph text
474    ///
475    /// Returns: a list of fonts and text ranges
476    pub fn get_fonts(&self) -> Vec<FontInfo> {
477        let mut result = Vec::new();
478        let mut set_fn = |fis: &[sb::skia_textlayout_Paragraph_FontInfo]| {
479            result = fis.iter().map(FontInfo::from_native_ref).collect();
480        };
481        unsafe { sb::C_Paragraph_getFonts(self.native(), VecSink::new(&mut set_fn).native_mut()) }
482        result
483    }
484}
485
486pub type VisitorInfo = Handle<sb::skia_textlayout_Paragraph_VisitorInfo>;
487
488impl NativeDrop for sb::skia_textlayout_Paragraph_VisitorInfo {
489    fn drop(&mut self) {
490        panic!("Internal error, Paragraph visitor can't be created in Rust")
491    }
492}
493
494impl fmt::Debug for VisitorInfo {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        f.debug_struct("VisitorInfo")
497            .field("font", &self.font())
498            .field("origin", &self.origin())
499            .field("advance_x", &self.advance_x())
500            .field("count", &self.count())
501            .field("glyphs", &self.glyphs())
502            .field("positions", &self.positions())
503            .field("utf8_starts", &self.utf8_starts())
504            .field("flags", &self.flags())
505            .finish()
506    }
507}
508
509impl VisitorInfo {
510    pub fn font(&self) -> &Font {
511        Font::from_native_ref(unsafe { &*self.native().font })
512    }
513
514    pub fn origin(&self) -> Point {
515        Point::from_native_c(self.native().origin)
516    }
517
518    pub fn advance_x(&self) -> scalar {
519        self.native().advanceX
520    }
521
522    pub fn count(&self) -> usize {
523        self.native().count as usize
524    }
525
526    pub fn glyphs(&self) -> &[GlyphId] {
527        unsafe { safer::from_raw_parts(self.native().glyphs, self.count()) }
528    }
529
530    pub fn positions(&self) -> &[Point] {
531        unsafe {
532            safer::from_raw_parts(
533                Point::from_native_ptr(self.native().positions),
534                self.count(),
535            )
536        }
537    }
538
539    pub fn utf8_starts(&self) -> &[u32] {
540        unsafe { safer::from_raw_parts(self.native().utf8Starts, self.count() + 1) }
541    }
542
543    pub fn flags(&self) -> VisitorFlags {
544        VisitorFlags::from_bits_truncate(self.native().flags)
545    }
546}
547
548pub type ExtendedVisitorInfo = Handle<sb::skia_textlayout_Paragraph_ExtendedVisitorInfo>;
549
550impl NativeDrop for sb::skia_textlayout_Paragraph_ExtendedVisitorInfo {
551    fn drop(&mut self) {
552        panic!("Internal error, Paragraph extended visitor info can't be created in Rust")
553    }
554}
555
556impl fmt::Debug for ExtendedVisitorInfo {
557    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558        f.debug_struct("VisitorInfo")
559            .field("font", &self.font())
560            .field("origin", &self.origin())
561            .field("advance", &self.advance())
562            .field("count", &self.count())
563            .field("glyphs", &self.glyphs())
564            .field("positions", &self.positions())
565            .field("bounds", &self.bounds())
566            .field("utf8_starts", &self.utf8_starts())
567            .field("flags", &self.flags())
568            .finish()
569    }
570}
571
572impl ExtendedVisitorInfo {
573    pub fn font(&self) -> &Font {
574        Font::from_native_ref(unsafe { &*self.native().font })
575    }
576
577    pub fn origin(&self) -> Point {
578        Point::from_native_c(self.native().origin)
579    }
580
581    pub fn advance(&self) -> Size {
582        Size::from_native_c(self.native().advance)
583    }
584
585    pub fn count(&self) -> usize {
586        self.native().count as usize
587    }
588
589    pub fn glyphs(&self) -> &[GlyphId] {
590        unsafe { safer::from_raw_parts(self.native().glyphs, self.count()) }
591    }
592
593    pub fn positions(&self) -> &[Point] {
594        unsafe {
595            safer::from_raw_parts(
596                Point::from_native_ptr(self.native().positions),
597                self.count(),
598            )
599        }
600    }
601
602    pub fn bounds(&self) -> &[Rect] {
603        let ptr = Rect::from_native_ptr(self.native().bounds);
604        unsafe { safer::from_raw_parts(ptr, self.count()) }
605    }
606
607    pub fn utf8_starts(&self) -> &[u32] {
608        unsafe { safer::from_raw_parts(self.native().utf8Starts, self.count() + 1) }
609    }
610
611    pub fn flags(&self) -> VisitorFlags {
612        VisitorFlags::from_bits_truncate(self.native().flags)
613    }
614}
615
616bitflags! {
617    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
618    pub struct VisitorFlags: u32 {
619        const WHITE_SPACE = sb::skia_textlayout_Paragraph_VisitorFlags_kWhiteSpace_VisitorFlag as _;
620    }
621}
622
623#[derive(Clone, PartialEq, Debug)]
624pub struct GlyphClusterInfo {
625    pub bounds: Rect,
626    pub text_range: TextRange,
627    pub position: TextDirection,
628}
629
630impl GlyphClusterInfo {
631    fn from_native_ref(native: &sb::skia_textlayout_Paragraph_GlyphClusterInfo) -> Self {
632        unsafe {
633            Self {
634                bounds: *Rect::from_native_ptr(&native.fBounds),
635                text_range: TextRange {
636                    start: native.fClusterTextRange.start,
637                    end: native.fClusterTextRange.end,
638                },
639                position: native.fGlyphClusterPosition,
640            }
641        }
642    }
643}
644
645/// The glyph and grapheme cluster information associated with a unicode
646/// codepoint in the paragraph.
647#[repr(C)]
648#[derive(Clone, PartialEq, Debug)]
649pub struct GlyphInfo {
650    pub grapheme_layout_bounds: Rect,
651    pub grapheme_cluster_text_range: TextRange,
652    pub text_direction: TextDirection,
653    pub is_ellipsis: bool,
654}
655native_transmutable!(
656    sb::skia_textlayout_Paragraph_GlyphInfo,
657    GlyphInfo,
658    glyph_info_layout
659);
660
661#[derive(Clone, PartialEq, Debug)]
662pub struct FontInfo {
663    pub font: Font,
664    pub text_range: TextRange,
665}
666
667impl FontInfo {
668    pub fn new(font: Font, text_range: TextRange) -> Self {
669        Self { font, text_range }
670    }
671
672    fn from_native_ref(native: &sb::skia_textlayout_Paragraph_FontInfo) -> Self {
673        Self {
674            font: Font::from_native_ref(&native.fFont).clone(),
675            text_range: TextRange {
676                start: native.fTextRange.start,
677                end: native.fTextRange.end,
678            },
679        }
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::Paragraph;
686    use crate::{
687        icu,
688        textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle},
689        FontMgr,
690    };
691
692    #[test]
693    #[serial_test::serial]
694    fn test_line_metrics() {
695        let paragraph = mk_lorem_ipsum_paragraph();
696        let line_metrics = paragraph.get_line_metrics();
697        for (line, lm) in line_metrics.iter().enumerate() {
698            println!("line {}: width: {}", line + 1, lm.width)
699        }
700    }
701
702    /// Regression test for <https://github.com/rust-skia/rust-skia/issues/585>
703    #[test]
704    #[serial_test::serial]
705    fn test_style_metrics() {
706        icu::init();
707
708        let mut style = ParagraphStyle::new();
709        let ts = TextStyle::new();
710        style.set_text_style(&ts);
711        let mut font_collection = FontCollection::new();
712        font_collection.set_default_font_manager(FontMgr::default(), None);
713        let mut paragraph_builder = ParagraphBuilder::new(&style, font_collection);
714        paragraph_builder.add_text("Lorem ipsum dolor sit amet\n");
715        let mut paragraph = paragraph_builder.build();
716        paragraph.layout(100.0);
717
718        let line_metrics = &paragraph.get_line_metrics()[0];
719        line_metrics.get_style_metrics(line_metrics.start_index..line_metrics.end_index);
720    }
721
722    #[test]
723    #[serial_test::serial]
724    fn test_font_infos() {
725        let paragraph = mk_lorem_ipsum_paragraph();
726        let infos = paragraph.get_fonts();
727        assert!(!infos.is_empty())
728    }
729
730    #[test]
731    #[serial_test::serial]
732    fn test_visit() {
733        let mut paragraph = mk_lorem_ipsum_paragraph();
734        let visitor = |line, info| {
735            println!("line {line}: {info:?}");
736        };
737        paragraph.visit(visitor);
738    }
739
740    #[test]
741    #[serial_test::serial]
742    fn test_extended_visit() {
743        let mut paragraph = mk_lorem_ipsum_paragraph();
744        let visitor = |line, info| {
745            println!("line {line}: {info:?}");
746        };
747        paragraph.extended_visit(visitor);
748    }
749
750    fn mk_lorem_ipsum_paragraph() -> Paragraph {
751        icu::init();
752
753        let mut font_collection = FontCollection::new();
754        font_collection.set_default_font_manager(FontMgr::new(), None);
755        let paragraph_style = ParagraphStyle::new();
756        let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
757        let ts = TextStyle::new();
758        paragraph_builder.push_style(&ts);
759        paragraph_builder.add_text(LOREM_IPSUM);
760        let mut paragraph = paragraph_builder.build();
761        paragraph.layout(256.0);
762
763        return paragraph;
764
765        static LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at leo at nulla tincidunt placerat. Proin eget purus augue. Quisque et est ullamcorper, pellentesque felis nec, pulvinar massa. Aliquam imperdiet, nulla ut dictum euismod, purus dui pulvinar risus, eu suscipit elit neque ac est. Nullam eleifend justo quis placerat ultricies. Vestibulum ut elementum velit. Praesent et dolor sit amet purus bibendum mattis. Aliquam erat volutpat.";
766    }
767
768    /// <https://github.com/rust-skia/rust-skia/issues/984>
769    #[test]
770    #[serial_test::serial]
771    fn skia_crash_macos() {
772        let mut font_collection = FontCollection::new();
773        font_collection.set_dynamic_font_manager(FontMgr::default());
774        let mut p = ParagraphBuilder::new(&ParagraphStyle::new(), font_collection);
775        p.add_text("👋test test 🦀");
776        let mut paragraph = p.build();
777        paragraph.layout(200.);
778    }
779}