1 | use std::{ffi, fmt, ops::Range}; |
2 | |
3 | use skia_bindings as sb; |
4 | |
5 | use super::{ |
6 | LineMetrics, PositionWithAffinity, RectHeightStyle, RectWidthStyle, TextBox, TextDirection, |
7 | TextIndex, TextRange, |
8 | }; |
9 | use crate::{ |
10 | interop::{Sink, VecSink}, |
11 | prelude::*, |
12 | scalar, Canvas, Font, Path, Point, Rect, Size, TextBlob, Unichar, |
13 | }; |
14 | |
15 | pub type Paragraph = RefHandle<sb::skia_textlayout_Paragraph>; |
16 | // <https://github.com/rust-skia/rust-skia/issues/537> |
17 | // unsafe_send_sync!(Paragraph); |
18 | |
19 | impl NativeDrop for sb::skia_textlayout_Paragraph { |
20 | fn drop(&mut self) { |
21 | unsafe { sb::C_Paragraph_delete(self) } |
22 | } |
23 | } |
24 | |
25 | impl fmt::Debug for Paragraph { |
26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
27 | f&mut DebugStruct<'_, '_>.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(name:"line_number" , &self.line_number()) |
37 | .finish() |
38 | } |
39 | } |
40 | |
41 | impl 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 | /// Returns: a number glyphs that could not be converted to path |
256 | pub fn get_path_at(&mut self, line_number: usize) -> (usize, Path) { |
257 | let mut path = Path::default(); |
258 | let unconverted_glyphs = unsafe { |
259 | sb::C_Paragraph_getPath( |
260 | self.native_mut(), |
261 | line_number.try_into().unwrap(), |
262 | path.native_mut(), |
263 | ) |
264 | }; |
265 | (unconverted_glyphs.try_into().unwrap(), path) |
266 | } |
267 | |
268 | /// Returns path for a text blob |
269 | /// |
270 | /// * `text_blob` - a text blob |
271 | /// Returns: a path |
272 | pub fn get_path(text_blob: &mut TextBlob) -> Path { |
273 | Path::construct(|p| unsafe { sb::C_Paragraph_GetPath(text_blob.native_mut(), p) }) |
274 | } |
275 | |
276 | /// Checks if a given text blob contains |
277 | /// glyph with emoji |
278 | /// |
279 | /// * `text_blob` - a text blob |
280 | /// Returns: `true` if there is such a glyph |
281 | pub fn contains_emoji(&mut self, text_blob: &mut TextBlob) -> bool { |
282 | unsafe { sb::C_Paragraph_containsEmoji(self.native_mut(), text_blob.native_mut()) } |
283 | } |
284 | |
285 | /// Checks if a given text blob contains colored font or bitmap |
286 | /// |
287 | /// * `text_blob` - a text blob |
288 | /// Returns: `true` if there is such a glyph |
289 | pub fn contains_color_font_or_bitmap(&mut self, text_blob: &mut TextBlob) -> bool { |
290 | unsafe { |
291 | sb::C_Paragraph_containsColorFontOrBitmap(self.native_mut(), text_blob.native_mut()) |
292 | } |
293 | } |
294 | |
295 | /// Finds the line number of the line that contains the given UTF-8 index. |
296 | /// |
297 | /// * `index` - a UTF-8 TextIndex into the paragraph |
298 | /// Returns: the line number the glyph that corresponds to the |
299 | /// given `code_unit_index` is in, or -1 if the `code_unit_index` |
300 | /// is out of bounds, or when the glyph is truncated or |
301 | /// ellipsized away. |
302 | pub fn get_line_number_at(&self, code_unit_index: TextIndex) -> Option<usize> { |
303 | // Returns -1 if `code_unit_index` is out of range. |
304 | unsafe { sb::C_Paragraph_getLineNumberAt(self.native(), code_unit_index) } |
305 | .try_into() |
306 | .ok() |
307 | } |
308 | |
309 | /// Finds the line number of the line that contains the given UTF-16 index. |
310 | /// |
311 | /// * `index` - a UTF-16 offset into the paragraph |
312 | /// Returns: the line number the glyph that corresponds to the |
313 | /// given `code_unit_index` is in, or -1 if the `code_unit_index` |
314 | /// is out of bounds, or when the glyph is truncated or |
315 | /// ellipsized away. |
316 | pub fn get_line_number_at_utf16_offset(&self, code_unit_index: TextIndex) -> Option<usize> { |
317 | // Returns -1 if `code_unit_index` is out of range. |
318 | unsafe { |
319 | sb::C_Paragraph_getLineNumberAtUTF16Offset(self.native_mut_force(), code_unit_index) |
320 | } |
321 | .try_into() |
322 | .ok() |
323 | } |
324 | |
325 | /// Returns line metrics info for the line |
326 | /// |
327 | /// * `line_number` - a line number |
328 | /// * `line_metrics` - an address to return the info (in case of null just skipped) |
329 | /// Returns: `true` if the line is found; `false` if not |
330 | pub fn get_line_metrics_at(&self, line_number: usize) -> Option<LineMetrics> { |
331 | let mut r = None; |
332 | let mut set_lm = |lm: &sb::skia_textlayout_LineMetrics| { |
333 | r = Some(LineMetrics::from_native_ref(lm)); |
334 | }; |
335 | unsafe { |
336 | sb::C_Paragraph_getLineMetricsAt( |
337 | self.native(), |
338 | line_number, |
339 | Sink::new(&mut set_lm).native_mut(), |
340 | ) |
341 | } |
342 | r |
343 | } |
344 | |
345 | /// Returns the visible text on the line (excluding a possible ellipsis) |
346 | /// |
347 | /// * `line_number` - a line number |
348 | /// * `include_spaces` - indicates if the whitespaces should be included |
349 | /// Returns: the range of the text that is shown in the line |
350 | pub fn get_actual_text_range(&self, line_number: usize, include_spaces: bool) -> TextRange { |
351 | let mut range = [0usize; 2]; |
352 | unsafe { |
353 | sb::C_Paragraph_getActualTextRange( |
354 | self.native(), |
355 | line_number, |
356 | include_spaces, |
357 | range.as_mut_ptr(), |
358 | ) |
359 | } |
360 | TextRange { |
361 | start: range[0], |
362 | end: range[1], |
363 | } |
364 | } |
365 | |
366 | /// Finds a glyph cluster for text index |
367 | /// |
368 | /// * `code_unit_index` - a text index |
369 | /// * `glyph_info` - a glyph cluster info filled if not null |
370 | /// Returns: `true` if glyph cluster was found; `false` if not |
371 | pub fn get_glyph_cluster_at(&self, code_unit_index: TextIndex) -> Option<GlyphClusterInfo> { |
372 | let mut r = None; |
373 | let mut set_fn = |gci: &sb::skia_textlayout_Paragraph_GlyphClusterInfo| { |
374 | r = Some(GlyphClusterInfo::from_native_ref(gci)) |
375 | }; |
376 | unsafe { |
377 | sb::C_Paragraph_getGlyphClusterAt( |
378 | self.native(), |
379 | code_unit_index, |
380 | Sink::new(&mut set_fn).native_mut(), |
381 | ) |
382 | } |
383 | r |
384 | } |
385 | |
386 | /// Finds the closest glyph cluster for a visual text position |
387 | /// |
388 | /// * `dx` - x coordinate |
389 | /// * `dy` - y coordinate |
390 | /// * `glyph_info` - a glyph cluster info filled if not null |
391 | /// Returns: `true` if glyph cluster was found; `false` if not |
392 | /// (which usually means the paragraph is empty) |
393 | pub fn get_closest_glyph_cluster_at(&self, d: impl Into<Point>) -> Option<GlyphClusterInfo> { |
394 | let mut r = None; |
395 | let mut set_fn = |gci: &sb::skia_textlayout_Paragraph_GlyphClusterInfo| { |
396 | r = Some(GlyphClusterInfo::from_native_ref(gci)) |
397 | }; |
398 | let d = d.into(); |
399 | unsafe { |
400 | sb::C_Paragraph_getClosestGlyphClusterAt( |
401 | self.native(), |
402 | d.x, |
403 | d.y, |
404 | Sink::new(&mut set_fn).native_mut(), |
405 | ) |
406 | } |
407 | r |
408 | } |
409 | |
410 | /// Retrieves the information associated with the glyph located at the given |
411 | /// `code_unit_index`. |
412 | /// |
413 | /// * `code_unit_index` - a UTF-16 offset into the paragraph |
414 | /// * `glyph_info` - an optional GlyphInfo struct to hold the |
415 | /// information associated with the glyph found at the |
416 | /// given index |
417 | /// Returns: `false` only if the offset is out of bounds |
418 | pub fn get_glyph_info_at_utf16_offset(&mut self, code_unit_index: usize) -> Option<GlyphInfo> { |
419 | GlyphInfo::try_construct(|gi| unsafe { |
420 | sb::C_Paragraph_getGlyphInfoAtUTF16Offset(self.native_mut(), code_unit_index, gi) |
421 | }) |
422 | } |
423 | |
424 | /// Finds the information associated with the closest glyph to the given |
425 | /// paragraph coordinates. |
426 | /// |
427 | /// * `d` - x/y coordinate |
428 | /// * `glyph_info` - an optional GlyphInfo struct to hold the |
429 | /// information associated with the glyph found. The |
430 | /// text indices and text ranges are described using |
431 | /// UTF-16 offsets |
432 | /// Returns: `true` if a grapheme cluster was found; `false` if not |
433 | /// (which usually means the paragraph is empty) |
434 | pub fn get_closest_utf16_glyph_info_at(&mut self, d: impl Into<Point>) -> Option<GlyphInfo> { |
435 | let d = d.into(); |
436 | GlyphInfo::try_construct(|gi| unsafe { |
437 | sb::C_Paragraph_getClosestUTF16GlyphInfoAt(self.native_mut(), d.x, d.y, gi) |
438 | }) |
439 | } |
440 | |
441 | /// Returns the font that is used to shape the text at the position |
442 | /// |
443 | /// * `code_unit_index` - text index |
444 | /// Returns: font info or an empty font info if the text is not found |
445 | pub fn get_font_at(&self, code_unit_index: TextIndex) -> Font { |
446 | Font::construct(|f| unsafe { sb::C_Paragraph_getFontAt(self.native(), code_unit_index, f) }) |
447 | } |
448 | |
449 | /// Returns the font used to shape the text at the given UTF-16 offset. |
450 | /// |
451 | /// * `code_unit_index` - a UTF-16 offset in the paragraph |
452 | /// Returns: font info or an empty font info if the text is not found |
453 | pub fn get_font_at_utf16_offset(&mut self, code_unit_index: usize) -> Font { |
454 | Font::construct(|f| unsafe { |
455 | sb::C_Paragraph_getFontAtUTF16Offset(self.native_mut(), code_unit_index, f) |
456 | }) |
457 | } |
458 | |
459 | /// Returns the information about all the fonts used to shape the paragraph text |
460 | /// |
461 | /// Returns: a list of fonts and text ranges |
462 | pub fn get_fonts(&self) -> Vec<FontInfo> { |
463 | let mut result = Vec::new(); |
464 | let mut set_fn = |fis: &[sb::skia_textlayout_Paragraph_FontInfo]| { |
465 | result = fis.iter().map(FontInfo::from_native_ref).collect(); |
466 | }; |
467 | unsafe { sb::C_Paragraph_getFonts(self.native(), VecSink::new(&mut set_fn).native_mut()) } |
468 | result |
469 | } |
470 | } |
471 | |
472 | pub type VisitorInfo = Handle<sb::skia_textlayout_Paragraph_VisitorInfo>; |
473 | |
474 | impl NativeDrop for sb::skia_textlayout_Paragraph_VisitorInfo { |
475 | fn drop(&mut self) { |
476 | panic!("Internal error, Paragraph visitor can't be created in Rust" ) |
477 | } |
478 | } |
479 | |
480 | impl fmt::Debug for VisitorInfo { |
481 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
482 | f&mut DebugStruct<'_, '_>.debug_struct("VisitorInfo" ) |
483 | .field("font" , &self.font()) |
484 | .field("origin" , &self.origin()) |
485 | .field("advance_x" , &self.advance_x()) |
486 | .field("count" , &self.count()) |
487 | .field("glyphs" , &self.glyphs()) |
488 | .field("positions" , &self.positions()) |
489 | .field("utf8_starts" , &self.utf8_starts()) |
490 | .field(name:"flags" , &self.flags()) |
491 | .finish() |
492 | } |
493 | } |
494 | |
495 | impl VisitorInfo { |
496 | pub fn font(&self) -> &Font { |
497 | Font::from_native_ref(unsafe { &*self.native().font }) |
498 | } |
499 | |
500 | pub fn origin(&self) -> Point { |
501 | Point::from_native_c(self.native().origin) |
502 | } |
503 | |
504 | pub fn advance_x(&self) -> scalar { |
505 | self.native().advanceX |
506 | } |
507 | |
508 | pub fn count(&self) -> usize { |
509 | self.native().count as usize |
510 | } |
511 | |
512 | pub fn glyphs(&self) -> &[u16] { |
513 | unsafe { safer::from_raw_parts(self.native().glyphs, self.count()) } |
514 | } |
515 | |
516 | pub fn positions(&self) -> &[Point] { |
517 | unsafe { |
518 | safer::from_raw_parts( |
519 | Point::from_native_ptr(self.native().positions), |
520 | self.count(), |
521 | ) |
522 | } |
523 | } |
524 | |
525 | pub fn utf8_starts(&self) -> &[u32] { |
526 | unsafe { safer::from_raw_parts(self.native().utf8Starts, self.count() + 1) } |
527 | } |
528 | |
529 | pub fn flags(&self) -> VisitorFlags { |
530 | VisitorFlags::from_bits_truncate(self.native().flags) |
531 | } |
532 | } |
533 | |
534 | pub type ExtendedVisitorInfo = Handle<sb::skia_textlayout_Paragraph_ExtendedVisitorInfo>; |
535 | |
536 | impl NativeDrop for sb::skia_textlayout_Paragraph_ExtendedVisitorInfo { |
537 | fn drop(&mut self) { |
538 | panic!("Internal error, Paragraph extended visitor info can't be created in Rust" ) |
539 | } |
540 | } |
541 | |
542 | impl fmt::Debug for ExtendedVisitorInfo { |
543 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
544 | f&mut DebugStruct<'_, '_>.debug_struct("VisitorInfo" ) |
545 | .field("font" , &self.font()) |
546 | .field("origin" , &self.origin()) |
547 | .field("advance" , &self.advance()) |
548 | .field("count" , &self.count()) |
549 | .field("glyphs" , &self.glyphs()) |
550 | .field("positions" , &self.positions()) |
551 | .field("bounds" , &self.bounds()) |
552 | .field("utf8_starts" , &self.utf8_starts()) |
553 | .field(name:"flags" , &self.flags()) |
554 | .finish() |
555 | } |
556 | } |
557 | |
558 | impl ExtendedVisitorInfo { |
559 | pub fn font(&self) -> &Font { |
560 | Font::from_native_ref(unsafe { &*self.native().font }) |
561 | } |
562 | |
563 | pub fn origin(&self) -> Point { |
564 | Point::from_native_c(self.native().origin) |
565 | } |
566 | |
567 | pub fn advance(&self) -> Size { |
568 | Size::from_native_c(self.native().advance) |
569 | } |
570 | |
571 | pub fn count(&self) -> usize { |
572 | self.native().count as usize |
573 | } |
574 | |
575 | pub fn glyphs(&self) -> &[u16] { |
576 | unsafe { safer::from_raw_parts(self.native().glyphs, self.count()) } |
577 | } |
578 | |
579 | pub fn positions(&self) -> &[Point] { |
580 | unsafe { |
581 | safer::from_raw_parts( |
582 | Point::from_native_ptr(self.native().positions), |
583 | self.count(), |
584 | ) |
585 | } |
586 | } |
587 | |
588 | pub fn bounds(&self) -> &[Rect] { |
589 | let ptr = Rect::from_native_ptr(self.native().bounds); |
590 | unsafe { safer::from_raw_parts(ptr, self.count()) } |
591 | } |
592 | |
593 | pub fn utf8_starts(&self) -> &[u32] { |
594 | unsafe { safer::from_raw_parts(self.native().utf8Starts, self.count() + 1) } |
595 | } |
596 | |
597 | pub fn flags(&self) -> VisitorFlags { |
598 | VisitorFlags::from_bits_truncate(self.native().flags) |
599 | } |
600 | } |
601 | |
602 | bitflags! { |
603 | #[derive (Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] |
604 | pub struct VisitorFlags: u32 { |
605 | const WHITE_SPACE = sb::skia_textlayout_Paragraph_VisitorFlags_kWhiteSpace_VisitorFlag as _; |
606 | } |
607 | } |
608 | |
609 | #[derive (Clone, PartialEq, Debug)] |
610 | pub struct GlyphClusterInfo { |
611 | pub bounds: Rect, |
612 | pub text_range: TextRange, |
613 | pub position: TextDirection, |
614 | } |
615 | |
616 | impl GlyphClusterInfo { |
617 | fn from_native_ref(native: &sb::skia_textlayout_Paragraph_GlyphClusterInfo) -> Self { |
618 | unsafe { |
619 | Self { |
620 | bounds: *Rect::from_native_ptr(&native.fBounds), |
621 | text_range: TextRange { |
622 | start: native.fClusterTextRange.start, |
623 | end: native.fClusterTextRange.end, |
624 | }, |
625 | position: native.fGlyphClusterPosition, |
626 | } |
627 | } |
628 | } |
629 | } |
630 | |
631 | /// The glyph and grapheme cluster information associated with a unicode |
632 | /// codepoint in the paragraph. |
633 | #[repr (C)] |
634 | #[derive (Clone, PartialEq, Debug)] |
635 | pub struct GlyphInfo { |
636 | pub grapheme_layout_bounds: Rect, |
637 | pub grapheme_cluster_text_range: TextRange, |
638 | pub text_direction: TextDirection, |
639 | pub is_ellipsis: bool, |
640 | } |
641 | native_transmutable!( |
642 | sb::skia_textlayout_Paragraph_GlyphInfo, |
643 | GlyphInfo, |
644 | glyph_info_layout |
645 | ); |
646 | |
647 | #[derive (Clone, PartialEq, Debug)] |
648 | pub struct FontInfo { |
649 | pub font: Font, |
650 | pub text_range: TextRange, |
651 | } |
652 | |
653 | impl FontInfo { |
654 | pub fn new(font: Font, text_range: TextRange) -> Self { |
655 | Self { font, text_range } |
656 | } |
657 | |
658 | fn from_native_ref(native: &sb::skia_textlayout_Paragraph_FontInfo) -> Self { |
659 | Self { |
660 | font: Font::from_native_ref(&native.fFont).clone(), |
661 | text_range: TextRange { |
662 | start: native.fTextRange.start, |
663 | end: native.fTextRange.end, |
664 | }, |
665 | } |
666 | } |
667 | } |
668 | |
669 | #[cfg (test)] |
670 | mod tests { |
671 | use super::Paragraph; |
672 | use crate::{ |
673 | icu, |
674 | textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle}, |
675 | FontMgr, |
676 | }; |
677 | |
678 | #[test ] |
679 | #[serial_test::serial] |
680 | fn test_line_metrics() { |
681 | let paragraph = mk_lorem_ipsum_paragraph(); |
682 | let line_metrics = paragraph.get_line_metrics(); |
683 | for (line, lm) in line_metrics.iter().enumerate() { |
684 | println!("line {}: width: {}" , line + 1, lm.width) |
685 | } |
686 | } |
687 | |
688 | /// Regression test for <https://github.com/rust-skia/rust-skia/issues/585> |
689 | #[test ] |
690 | #[serial_test::serial] |
691 | fn test_style_metrics() { |
692 | icu::init(); |
693 | |
694 | let mut style = ParagraphStyle::new(); |
695 | let ts = TextStyle::new(); |
696 | style.set_text_style(&ts); |
697 | let mut font_collection = FontCollection::new(); |
698 | font_collection.set_default_font_manager(FontMgr::default(), None); |
699 | let mut paragraph_builder = ParagraphBuilder::new(&style, font_collection); |
700 | paragraph_builder.add_text("Lorem ipsum dolor sit amet \n" ); |
701 | let mut paragraph = paragraph_builder.build(); |
702 | paragraph.layout(100.0); |
703 | |
704 | let line_metrics = ¶graph.get_line_metrics()[0]; |
705 | line_metrics.get_style_metrics(line_metrics.start_index..line_metrics.end_index); |
706 | } |
707 | |
708 | #[test ] |
709 | #[serial_test::serial] |
710 | fn test_font_infos() { |
711 | let paragraph = mk_lorem_ipsum_paragraph(); |
712 | let infos = paragraph.get_fonts(); |
713 | assert!(!infos.is_empty()) |
714 | } |
715 | |
716 | #[test ] |
717 | #[serial_test::serial] |
718 | fn test_visit() { |
719 | let mut paragraph = mk_lorem_ipsum_paragraph(); |
720 | let visitor = |line, info| { |
721 | println!("line {}: {:?}" , line, info); |
722 | }; |
723 | paragraph.visit(visitor); |
724 | } |
725 | |
726 | #[test ] |
727 | #[serial_test::serial] |
728 | fn test_extended_visit() { |
729 | let mut paragraph = mk_lorem_ipsum_paragraph(); |
730 | let visitor = |line, info| { |
731 | println!("line {}: {:?}" , line, info); |
732 | }; |
733 | paragraph.extended_visit(visitor); |
734 | } |
735 | |
736 | fn mk_lorem_ipsum_paragraph() -> Paragraph { |
737 | icu::init(); |
738 | |
739 | let mut font_collection = FontCollection::new(); |
740 | font_collection.set_default_font_manager(FontMgr::new(), None); |
741 | let paragraph_style = ParagraphStyle::new(); |
742 | let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); |
743 | let ts = TextStyle::new(); |
744 | paragraph_builder.push_style(&ts); |
745 | paragraph_builder.add_text(LOREM_IPSUM); |
746 | let mut paragraph = paragraph_builder.build(); |
747 | paragraph.layout(256.0); |
748 | |
749 | return paragraph; |
750 | |
751 | 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." ; |
752 | } |
753 | } |
754 | |