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!(sb::skia_textlayout_Paragraph_GlyphInfo, GlyphInfo);
656
657#[derive(Clone, PartialEq, Debug)]
658pub struct FontInfo {
659    pub font: Font,
660    pub text_range: TextRange,
661}
662
663impl FontInfo {
664    pub fn new(font: Font, text_range: TextRange) -> Self {
665        Self { font, text_range }
666    }
667
668    fn from_native_ref(native: &sb::skia_textlayout_Paragraph_FontInfo) -> Self {
669        Self {
670            font: Font::from_native_ref(&native.fFont).clone(),
671            text_range: TextRange {
672                start: native.fTextRange.start,
673                end: native.fTextRange.end,
674            },
675        }
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use super::Paragraph;
682    use crate::{
683        icu,
684        textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle},
685        FontMgr,
686    };
687
688    #[test]
689    #[serial_test::serial]
690    fn test_line_metrics() {
691        let paragraph = mk_lorem_ipsum_paragraph();
692        let line_metrics = paragraph.get_line_metrics();
693        for (line, lm) in line_metrics.iter().enumerate() {
694            println!("line {}: width: {}", line + 1, lm.width)
695        }
696    }
697
698    /// Regression test for <https://github.com/rust-skia/rust-skia/issues/585>
699    #[test]
700    #[serial_test::serial]
701    fn test_style_metrics() {
702        icu::init();
703
704        let mut style = ParagraphStyle::new();
705        let ts = TextStyle::new();
706        style.set_text_style(&ts);
707        let mut font_collection = FontCollection::new();
708        font_collection.set_default_font_manager(FontMgr::default(), None);
709        let mut paragraph_builder = ParagraphBuilder::new(&style, font_collection);
710        paragraph_builder.add_text("Lorem ipsum dolor sit amet\n");
711        let mut paragraph = paragraph_builder.build();
712        paragraph.layout(100.0);
713
714        let line_metrics = &paragraph.get_line_metrics()[0];
715        line_metrics.get_style_metrics(line_metrics.start_index..line_metrics.end_index);
716    }
717
718    #[test]
719    #[serial_test::serial]
720    fn test_font_infos() {
721        let paragraph = mk_lorem_ipsum_paragraph();
722        let infos = paragraph.get_fonts();
723        assert!(!infos.is_empty())
724    }
725
726    #[test]
727    #[serial_test::serial]
728    fn test_visit() {
729        let mut paragraph = mk_lorem_ipsum_paragraph();
730        let visitor = |line, info| {
731            println!("line {line}: {info:?}");
732        };
733        paragraph.visit(visitor);
734    }
735
736    #[test]
737    #[serial_test::serial]
738    fn test_extended_visit() {
739        let mut paragraph = mk_lorem_ipsum_paragraph();
740        let visitor = |line, info| {
741            println!("line {line}: {info:?}");
742        };
743        paragraph.extended_visit(visitor);
744    }
745
746    fn mk_lorem_ipsum_paragraph() -> Paragraph {
747        icu::init();
748
749        let mut font_collection = FontCollection::new();
750        font_collection.set_default_font_manager(FontMgr::new(), None);
751        let paragraph_style = ParagraphStyle::new();
752        let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
753        let ts = TextStyle::new();
754        paragraph_builder.push_style(&ts);
755        paragraph_builder.add_text(LOREM_IPSUM);
756        let mut paragraph = paragraph_builder.build();
757        paragraph.layout(256.0);
758
759        return paragraph;
760
761        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.";
762    }
763
764    /// <https://github.com/rust-skia/rust-skia/issues/984>
765    #[test]
766    #[serial_test::serial]
767    fn skia_crash_macos() {
768        let mut font_collection = FontCollection::new();
769        font_collection.set_dynamic_font_manager(FontMgr::default());
770        let mut p = ParagraphBuilder::new(&ParagraphStyle::new(), font_collection);
771        p.add_text("👋test test 🦀");
772        let mut paragraph = p.build();
773        paragraph.layout(200.);
774    }
775}