Skip to main content

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