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>;
16impl 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 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 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 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 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 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 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 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 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 pub fn get_line_number_at(&self, code_unit_index: TextIndex) -> Option<usize> {
308 unsafe { sb::C_Paragraph_getLineNumberAt(self.native(), code_unit_index) }
310 .try_into()
311 .ok()
312 }
313
314 pub fn get_line_number_at_utf16_offset(&self, code_unit_index: TextIndex) -> Option<usize> {
323 unsafe {
325 sb::C_Paragraph_getLineNumberAtUTF16Offset(self.native_mut_force(), code_unit_index)
326 }
327 .try_into()
328 .ok()
329 }
330
331 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 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 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 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 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 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 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 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 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#[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 #[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 = ¶graph.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(¶graph_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 #[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}