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