1use std::fmt;
10use std::ops::Range;
11
12use crate::fonts::FontCollection;
13use crate::text::{
14 FontFeature, FontStyle, FontVariation, FontWeight, TextAlign, TextBaseline, TextDecoration,
15 TextDecorationStyle, TextDirection, TextHeightBehavior, TextLeadingDistribution,
16};
17use crate::{Canvas, Color, Offset, Paint, Rect, Shadow};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct TextPosition {
27 pub offset: i32,
30 pub affinity: TextAffinity,
33}
34
35impl TextPosition {
36 pub const fn new(offset: i32) -> TextPosition {
39 TextPosition {
40 offset,
41 affinity: TextAffinity::Downstream,
42 }
43 }
44
45 pub const fn with_affinity(offset: i32, affinity: TextAffinity) -> TextPosition {
47 TextPosition { offset, affinity }
48 }
49}
50
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
54pub enum TextAffinity {
55 Upstream,
58 #[default]
61 Downstream,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66pub struct TextRange {
67 pub start: i32,
71 pub end: i32,
73}
74
75impl TextRange {
76 pub const EMPTY: TextRange = TextRange { start: -1, end: -1 };
78
79 pub const fn new(start: i32, end: i32) -> TextRange {
81 debug_assert!(start >= -1);
82 debug_assert!(end >= -1);
83 TextRange { start, end }
84 }
85
86 pub const fn collapsed(offset: i32) -> TextRange {
88 debug_assert!(offset >= -1);
89 TextRange {
90 start: offset,
91 end: offset,
92 }
93 }
94
95 pub fn is_valid(&self) -> bool {
97 self.start >= 0 && self.end >= 0
98 }
99
100 pub fn is_collapsed(&self) -> bool {
102 self.start == self.end
103 }
104
105 pub fn is_normalized(&self) -> bool {
107 self.end >= self.start
108 }
109
110 pub fn text_before<'a>(&self, text: &'a str) -> &'a str {
112 debug_assert!(self.is_normalized());
113 let start = byte_of_code_unit(text, self.start as usize);
114 &text[..start]
115 }
116
117 pub fn text_after<'a>(&self, text: &'a str) -> &'a str {
119 debug_assert!(self.is_normalized());
120 let end = byte_of_code_unit(text, self.end as usize);
121 &text[end..]
122 }
123
124 pub fn text_inside<'a>(&self, text: &'a str) -> &'a str {
126 debug_assert!(self.is_normalized());
127 let start = byte_of_code_unit(text, self.start as usize);
128 let end = byte_of_code_unit(text, self.end as usize);
129 &text[start..end]
130 }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq)]
137pub struct ParagraphConstraints {
138 pub width: f64,
145}
146
147impl ParagraphConstraints {
148 pub const fn new(width: f64) -> ParagraphConstraints {
150 ParagraphConstraints { width }
151 }
152}
153
154#[derive(Clone, Copy, Debug, PartialEq)]
158pub struct TextBox {
159 pub left: f64,
161 pub top: f64,
163 pub right: f64,
165 pub bottom: f64,
167 pub direction: TextDirection,
169}
170
171impl TextBox {
172 pub const fn from_ltrbd(
174 left: f64,
175 top: f64,
176 right: f64,
177 bottom: f64,
178 direction: TextDirection,
179 ) -> TextBox {
180 TextBox {
181 left,
182 top,
183 right,
184 bottom,
185 direction,
186 }
187 }
188
189 pub fn to_rect(&self) -> Rect {
191 Rect::from_ltrb(self.left, self.top, self.right, self.bottom)
192 }
193
194 pub fn start(&self) -> f64 {
197 match self.direction {
198 TextDirection::Ltr => self.left,
199 TextDirection::Rtl => self.right,
200 }
201 }
202
203 pub fn end(&self) -> f64 {
206 match self.direction {
207 TextDirection::Ltr => self.right,
208 TextDirection::Rtl => self.left,
209 }
210 }
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
215pub enum PlaceholderAlignment {
216 Baseline,
217 AboveBaseline,
218 BelowBaseline,
219 Top,
220 Bottom,
221 Middle,
222}
223
224#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
227pub enum BoxHeightStyle {
228 #[default]
229 Tight,
230 Max,
231 IncludeLineSpacingMiddle,
232 IncludeLineSpacingTop,
233 IncludeLineSpacingBottom,
234 Strut,
235}
236
237#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
240pub enum BoxWidthStyle {
241 #[default]
242 Tight,
243 Max,
244}
245
246#[derive(Clone, Copy, Debug, PartialEq)]
249pub struct GlyphInfo {
250 pub grapheme_cluster_layout_bounds: Rect,
252 pub grapheme_cluster_code_unit_range: TextRange,
254 pub writing_direction: TextDirection,
256}
257
258#[derive(Clone, Copy, Debug, PartialEq)]
260pub struct LineMetrics {
261 pub hard_break: bool,
264 pub ascent: f64,
267 pub descent: f64,
270 pub unscaled_ascent: f64,
273 pub height: f64,
275 pub width: f64,
278 pub left: f64,
280 pub baseline: f64,
282 pub line_number: i32,
284}
285
286#[derive(Clone, Debug, Default, PartialEq)]
291pub struct TextStyle {
292 pub color: Option<Color>,
293 pub decoration: Option<TextDecoration>,
294 pub decoration_color: Option<Color>,
295 pub decoration_style: Option<TextDecorationStyle>,
296 pub decoration_thickness: Option<f64>,
297 pub font_weight: Option<FontWeight>,
298 pub font_style: Option<FontStyle>,
299 pub text_baseline: Option<TextBaseline>,
300 pub font_family: Option<String>,
301 pub font_family_fallback: Option<Vec<String>>,
302 pub font_size: Option<f64>,
303 pub letter_spacing: Option<f64>,
304 pub word_spacing: Option<f64>,
305 pub height: Option<f64>,
306 pub leading_distribution: Option<TextLeadingDistribution>,
307 pub background: Option<Paint>,
308 pub foreground: Option<Paint>,
309 pub shadows: Option<Vec<Shadow>>,
310 pub font_features: Option<Vec<FontFeature>>,
311 pub font_variations: Option<Vec<FontVariation>>,
312}
313
314impl TextStyle {
315 pub fn new() -> TextStyle {
317 TextStyle::default()
318 }
319
320 fn merged_with(&self, other: &TextStyle) -> TextStyle {
323 TextStyle {
324 color: other.color.or(self.color),
325 decoration: other.decoration.or(self.decoration),
326 decoration_color: other.decoration_color.or(self.decoration_color),
327 decoration_style: other.decoration_style.or(self.decoration_style),
328 decoration_thickness: other.decoration_thickness.or(self.decoration_thickness),
329 font_weight: other.font_weight.or(self.font_weight),
330 font_style: other.font_style.or(self.font_style),
331 text_baseline: other.text_baseline.or(self.text_baseline),
332 font_family: other
333 .font_family
334 .clone()
335 .or_else(|| self.font_family.clone()),
336 font_family_fallback: other
337 .font_family_fallback
338 .clone()
339 .or_else(|| self.font_family_fallback.clone()),
340 font_size: other.font_size.or(self.font_size),
341 letter_spacing: other.letter_spacing.or(self.letter_spacing),
342 word_spacing: other.word_spacing.or(self.word_spacing),
343 height: other.height.or(self.height),
344 leading_distribution: other.leading_distribution.or(self.leading_distribution),
345 background: other.background.clone().or_else(|| self.background.clone()),
346 foreground: other.foreground.clone().or_else(|| self.foreground.clone()),
347 shadows: other.shadows.clone().or_else(|| self.shadows.clone()),
348 font_features: other
349 .font_features
350 .clone()
351 .or_else(|| self.font_features.clone()),
352 font_variations: other
353 .font_variations
354 .clone()
355 .or_else(|| self.font_variations.clone()),
356 }
357 }
358
359 fn to_native(&self) -> valo::TextStyle {
361 let mut style = valo::TextStyle::default();
362 let mut families = Vec::new();
363 if let Some(family) = &self.font_family {
364 families.push(family.clone());
365 }
366 if let Some(fallback) = &self.font_family_fallback {
367 families.extend(fallback.iter().cloned());
368 }
369 style.families = families;
370 if let Some(weight) = self.font_weight {
371 style.weight = weight.value as u16;
372 }
373 style.italic = self.font_style == Some(FontStyle::Italic);
374 if let Some(size) = self.font_size {
375 style.size = size as f32;
376 }
377 if let Some(foreground) = &self.foreground {
378 style.color = foreground.color;
379 } else if let Some(color) = self.color {
380 style.color = color.into();
381 }
382 if let Some(letter_spacing) = self.letter_spacing {
383 style.letter_spacing = letter_spacing as f32;
384 }
385 if let Some(word_spacing) = self.word_spacing {
386 style.word_spacing = word_spacing as f32;
387 }
388 style.height = match self.height {
389 Some(height) if height != crate::text::K_TEXT_HEIGHT_NONE => Some(height as f32),
390 _ => None,
391 };
392 style.decoration = native_decoration(
393 self.decoration,
394 self.decoration_color,
395 self.decoration_thickness,
396 );
397 if let Some(shadows) = &self.shadows {
398 style.shadows = shadows
399 .iter()
400 .map(|shadow| valo::Shadow {
401 color: shadow.color.into(),
402 offset: shadow.offset.into(),
403 blur: shadow.blur_sigma() as f32,
404 })
405 .collect();
406 }
407 style
408 }
409}
410
411fn native_decoration(
413 decoration: Option<TextDecoration>,
414 color: Option<Color>,
415 thickness: Option<f64>,
416) -> Option<valo::Decoration> {
417 let decoration = decoration?;
418 let kind = if decoration.contains(TextDecoration::UNDERLINE) {
419 valo::DecorationKind::Underline
420 } else if decoration.contains(TextDecoration::OVERLINE) {
421 valo::DecorationKind::Overline
422 } else if decoration.contains(TextDecoration::LINE_THROUGH) {
423 valo::DecorationKind::LineThrough
424 } else {
425 return None;
426 };
427 Some(valo::Decoration {
428 kind,
429 color: color.map(Into::into),
430 thickness: thickness.unwrap_or(1.0) as f32,
431 })
432}
433
434#[derive(Clone, Debug, Default, PartialEq)]
440pub struct ParagraphStyle {
441 pub text_align: Option<TextAlign>,
442 pub text_direction: Option<TextDirection>,
443 pub max_lines: Option<i32>,
444 pub font_family: Option<String>,
445 pub font_size: Option<f64>,
446 pub height: Option<f64>,
447 pub text_height_behavior: Option<TextHeightBehavior>,
448 pub font_weight: Option<FontWeight>,
449 pub font_style: Option<FontStyle>,
450 pub ellipsis: Option<String>,
451}
452
453impl ParagraphStyle {
454 pub fn new() -> ParagraphStyle {
456 ParagraphStyle::default()
457 }
458
459 fn base_text_style(&self) -> TextStyle {
461 TextStyle {
462 font_family: self.font_family.clone(),
463 font_size: self.font_size,
464 height: self.height,
465 font_weight: self.font_weight,
466 font_style: self.font_style,
467 ..TextStyle::default()
468 }
469 }
470
471 fn to_native(&self) -> valo::ParagraphStyle {
472 valo::ParagraphStyle {
473 align: native_align(self.text_align, self.text_direction),
474 direction: self.text_direction.map(native_direction),
475 preserve_trailing_whitespace: false,
476 max_lines: self.max_lines.map(|max_lines| max_lines as u32),
477 ellipsis: self.ellipsis.clone(),
478 }
479 }
480}
481
482fn native_align(align: Option<TextAlign>, direction: Option<TextDirection>) -> valo::TextAlign {
485 match align {
486 None | Some(TextAlign::Left) => valo::TextAlign::Left,
487 Some(TextAlign::Right) => valo::TextAlign::Right,
488 Some(TextAlign::Center) => valo::TextAlign::Center,
489 Some(TextAlign::Justify) => valo::TextAlign::Justify,
490 Some(TextAlign::Start) => match direction {
491 Some(TextDirection::Rtl) => valo::TextAlign::Right,
492 _ => valo::TextAlign::Left,
493 },
494 Some(TextAlign::End) => match direction {
495 Some(TextDirection::Rtl) => valo::TextAlign::Left,
496 _ => valo::TextAlign::Right,
497 },
498 }
499}
500
501fn native_direction(direction: TextDirection) -> valo::TextDirection {
502 match direction {
503 TextDirection::Ltr => valo::TextDirection::Ltr,
504 TextDirection::Rtl => valo::TextDirection::Rtl,
505 }
506}
507
508pub struct ParagraphBuilder {
522 paragraph_style: ParagraphStyle,
523 style_stack: Vec<TextStyle>,
524 runs: Vec<(String, valo::TextStyle)>,
525}
526
527impl ParagraphBuilder {
528 pub fn new(style: ParagraphStyle) -> ParagraphBuilder {
530 let base = style.base_text_style();
531 ParagraphBuilder {
532 paragraph_style: style,
533 style_stack: vec![base],
534 runs: Vec::new(),
535 }
536 }
537
538 pub fn placeholder_count(&self) -> i32 {
542 0
543 }
544
545 pub fn push_style(&mut self, style: TextStyle) {
549 let current = self
550 .style_stack
551 .last()
552 .expect("the base style is never popped");
553 let merged = current.merged_with(&style);
554 self.style_stack.push(merged);
555 }
556
557 pub fn pop(&mut self) {
563 if self.style_stack.len() > 1 {
564 self.style_stack.pop();
565 }
566 }
567
568 pub fn add_text(&mut self, text: &str) {
572 let style = self
573 .style_stack
574 .last()
575 .expect("the base style is never popped")
576 .to_native();
577 self.runs.push((text.to_owned(), style));
578 }
579
580 pub fn build(self, fonts: &mut FontCollection) -> Paragraph {
586 let direction = self
587 .paragraph_style
588 .text_direction
589 .unwrap_or(TextDirection::Ltr);
590 let mut builder = valo::ParagraphBuilder::new(fonts);
591 builder.style(self.paragraph_style.to_native());
592 for (text, style) in &self.runs {
593 builder.add_text(text, style);
594 }
595 let inner = builder.build();
596 Paragraph {
597 code_units: CodeUnits::of(inner.text()),
598 inner,
599 direction,
600 layout_width: 0.0,
601 disposed: false,
602 }
603 }
604}
605
606#[derive(Clone, Debug)]
608struct CodeUnits {
609 byte_of: Vec<usize>,
611 len: usize,
613}
614
615impl CodeUnits {
616 fn of(text: &str) -> CodeUnits {
617 let mut byte_of = Vec::with_capacity(text.len() + 1);
618 for (byte, ch) in text.char_indices() {
619 byte_of.push(byte);
620 if ch.len_utf16() == 2 {
621 byte_of.push(byte);
623 }
624 }
625 byte_of.push(text.len());
626 let len = byte_of.len() - 1;
627 CodeUnits { byte_of, len }
628 }
629
630 fn byte(&self, code_unit: i32) -> usize {
631 let clamped = code_unit.clamp(0, self.len as i32) as usize;
632 self.byte_of[clamped]
633 }
634
635 fn code_unit(&self, byte: usize) -> i32 {
637 let first_at_or_after = self.byte_of.partition_point(|candidate| *candidate < byte);
638 match self.byte_of.get(first_at_or_after) {
639 Some(candidate) if *candidate == byte => first_at_or_after as i32,
640 _ => first_at_or_after.saturating_sub(1) as i32,
641 }
642 }
643}
644
645fn byte_of_code_unit(text: &str, code_unit: usize) -> usize {
647 CodeUnits::of(text).byte(code_unit as i32)
648}
649
650pub struct Paragraph {
659 inner: valo::Paragraph,
660 code_units: CodeUnits,
661 direction: TextDirection,
662 layout_width: f64,
664 disposed: bool,
665}
666
667impl Paragraph {
668 pub fn width(&self) -> f64 {
674 self.layout_width
675 }
676
677 pub fn height(&self) -> f64 {
681 f64::from(self.inner.height())
682 }
683
684 pub fn longest_line(&self) -> f64 {
687 f64::from(self.inner.longest_line())
688 }
689
690 pub fn min_intrinsic_width(&self) -> f64 {
693 f64::from(self.inner.min_intrinsic_width())
694 }
695
696 pub fn max_intrinsic_width(&self) -> f64 {
699 f64::from(self.inner.max_intrinsic_width())
700 }
701
702 pub fn alphabetic_baseline(&self) -> f64 {
705 self.inner
706 .lines()
707 .first()
708 .map_or(0.0, |line| f64::from(line.baseline))
709 }
710
711 pub fn ideographic_baseline(&self) -> f64 {
716 self.inner
717 .lines()
718 .first()
719 .map_or(0.0, |line| f64::from(line.baseline + line.descent))
720 }
721
722 pub fn did_exceed_max_lines(&self) -> bool {
726 self.inner.truncated()
727 }
728
729 pub fn layout(&mut self, constraints: ParagraphConstraints) {
733 debug_assert!(!self.disposed);
734 self.layout_width = constraints.width;
735 self.inner.layout(constraints.width as f32);
736 }
737
738 pub fn get_boxes_for_range(
743 &self,
744 start: i32,
745 end: i32,
746 box_height_style: BoxHeightStyle,
747 box_width_style: BoxWidthStyle,
748 ) -> Vec<TextBox> {
749 let _ = (box_height_style, box_width_style);
750 let range = self.code_units.byte(start)..self.code_units.byte(end);
751 self.inner
752 .rects_for_range(range)
753 .into_iter()
754 .map(|rect| self.text_box(rect))
755 .collect()
756 }
757
758 fn text_box(&self, rect: valo::Rect) -> TextBox {
759 TextBox::from_ltrbd(
760 f64::from(rect.x),
761 f64::from(rect.y),
762 f64::from(rect.x + rect.width),
763 f64::from(rect.y + rect.height),
764 self.direction,
765 )
766 }
767
768 pub fn get_boxes_for_placeholders(&self) -> Vec<TextBox> {
772 Vec::new()
773 }
774
775 pub fn get_position_for_offset(&self, offset: Offset) -> TextPosition {
777 let position = self.inner.glyph_position_at(offset.into());
778 TextPosition::with_affinity(
779 self.code_units.code_unit(position.offset),
780 if position.downstream {
781 TextAffinity::Downstream
782 } else {
783 TextAffinity::Upstream
784 },
785 )
786 }
787
788 pub fn get_closest_glyph_info_for_offset(&self, offset: Offset) -> Option<GlyphInfo> {
792 let position = self.inner.glyph_position_at(offset.into());
793 let anchor = if position.downstream {
794 position.offset
795 } else {
796 position.offset.saturating_sub(1)
797 };
798 self.glyph_info_at_byte(anchor)
799 }
800
801 pub fn get_glyph_info_at(&self, code_unit_offset: i32) -> Option<GlyphInfo> {
804 if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
805 return None;
806 }
807 self.glyph_info_at_byte(self.code_units.byte(code_unit_offset))
808 }
809
810 fn glyph_info_at_byte(&self, byte: usize) -> Option<GlyphInfo> {
813 for line in self.inner.lines() {
814 if !line.range.contains(&byte) {
815 continue;
816 }
817 let top = line.baseline - line.ascent;
818 let bottom = line.baseline + line.descent;
819 let cluster_starts = cluster_starts(line);
820 for run in &line.runs {
821 for glyph in &run.glyphs {
822 let cluster = cluster_range(&cluster_starts, glyph.cluster, line.range.end);
823 if !cluster.contains(&byte) {
824 continue;
825 }
826 let (left, right) = if glyph.advance >= 0.0 {
827 (glyph.x, glyph.x + glyph.advance)
828 } else {
829 (glyph.x + glyph.advance, glyph.x)
830 };
831 return Some(GlyphInfo {
832 grapheme_cluster_layout_bounds: Rect::from_ltrb(
833 f64::from(left),
834 f64::from(top),
835 f64::from(right),
836 f64::from(bottom),
837 ),
838 grapheme_cluster_code_unit_range: TextRange::new(
839 self.code_units.code_unit(cluster.start),
840 self.code_units.code_unit(cluster.end),
841 ),
842 writing_direction: if run.rtl {
843 TextDirection::Rtl
844 } else {
845 TextDirection::Ltr
846 },
847 });
848 }
849 }
850 }
851 None
852 }
853
854 pub fn get_word_boundary(&self, position: TextPosition) -> TextRange {
856 let range = self
857 .inner
858 .word_boundary(self.code_units.byte(position.offset));
859 self.text_range(range)
860 }
861
862 pub fn get_line_boundary(&self, position: TextPosition) -> TextRange {
866 let byte = self.code_units.byte(position.offset);
867 let Some(line) = self.line_containing(byte, position.affinity) else {
868 return TextRange::EMPTY;
869 };
870 let text = self.inner.text();
871 let mut end = line.range.end;
872 if end > line.range.start && text.as_bytes().get(end - 1) == Some(&b'\n') {
873 end -= 1;
874 }
875 self.text_range(line.range.start..end)
876 }
877
878 fn line_containing(&self, byte: usize, affinity: TextAffinity) -> Option<&valo::Line> {
879 let lines = self.inner.lines();
880 let index = lines.iter().position(|line| {
881 let starts_next_line = byte == line.range.start && affinity == TextAffinity::Upstream;
882 (line.range.contains(&byte) && !starts_next_line) || byte == line.range.end
883 });
884 index.and_then(|index| lines.get(index)).or_else(|| {
885 let last = lines.last()?;
886 (byte >= last.range.end).then_some(last)
887 })
888 }
889
890 fn text_range(&self, range: Range<usize>) -> TextRange {
891 TextRange::new(
892 self.code_units.code_unit(range.start),
893 self.code_units.code_unit(range.end),
894 )
895 }
896
897 pub fn compute_line_metrics(&self) -> Vec<LineMetrics> {
900 let lines = self.inner.lines();
901 let text = self.inner.text();
902 lines
903 .iter()
904 .enumerate()
905 .map(|(index, line)| {
906 let is_last = index + 1 == lines.len();
907 let ends_with_newline = line.range.end > line.range.start
908 && text.as_bytes().get(line.range.end - 1) == Some(&b'\n');
909 LineMetrics {
910 hard_break: is_last || ends_with_newline,
911 ascent: f64::from(line.ascent),
912 descent: f64::from(line.descent),
913 unscaled_ascent: f64::from(line.ascent),
914 height: f64::from(line.ascent + line.descent),
915 width: f64::from(line.width),
916 left: f64::from(line.left),
917 baseline: f64::from(line.baseline),
918 line_number: index as i32,
919 }
920 })
921 .collect()
922 }
923
924 pub fn get_line_metrics_at(&self, line_number: i32) -> Option<LineMetrics> {
927 if line_number < 0 {
928 return None;
929 }
930 self.compute_line_metrics()
931 .into_iter()
932 .nth(line_number as usize)
933 }
934
935 pub fn number_of_lines(&self) -> i32 {
940 self.inner.lines().len() as i32
941 }
942
943 pub fn get_line_number_at(&self, code_unit_offset: i32) -> Option<i32> {
946 if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
947 return None;
948 }
949 let byte = self.code_units.byte(code_unit_offset);
950 self.inner
951 .lines()
952 .iter()
953 .position(|line| line.range.contains(&byte))
954 .map(|index| index as i32)
955 }
956
957 pub fn dispose(&mut self) {
960 debug_assert!(!self.disposed);
961 self.disposed = true;
962 }
963
964 pub fn debug_disposed(&self) -> bool {
966 self.disposed
967 }
968
969 pub(crate) fn native(&self) -> &valo::Paragraph {
971 &self.inner
972 }
973}
974
975fn cluster_starts(line: &valo::Line) -> Vec<usize> {
978 let mut starts: Vec<usize> = line
979 .runs
980 .iter()
981 .flat_map(|run| run.glyphs.iter().map(|glyph| glyph.cluster))
982 .collect();
983 starts.sort_unstable();
984 starts.dedup();
985 starts
986}
987
988fn cluster_range(cluster_starts: &[usize], start: usize, line_end: usize) -> Range<usize> {
989 let next = cluster_starts
990 .iter()
991 .find(|candidate| **candidate > start)
992 .copied()
993 .unwrap_or(line_end);
994 start..next.max(start + 1)
995}
996
997impl fmt::Debug for Paragraph {
998 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
999 f.debug_struct("Paragraph")
1000 .field("text", &self.inner.text())
1001 .field("width", &self.width())
1002 .field("height", &self.height())
1003 .finish()
1004 }
1005}
1006
1007pub trait CanvasText {
1009 fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset);
1013}
1014
1015impl CanvasText for Canvas {
1016 fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset) {
1017 valo::DrawParagraphExt::draw_paragraph(self, paragraph.native(), offset);
1018 }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024
1025 #[test]
1026 fn code_units_map_surrogate_pairs_to_one_character() {
1027 let units = CodeUnits::of("a😀b");
1028 assert_eq!(units.len, 4);
1029 assert_eq!(units.byte(0), 0);
1030 assert_eq!(units.byte(1), 1);
1031 assert_eq!(
1032 units.byte(2),
1033 1,
1034 "the second half of the pair maps to the same byte"
1035 );
1036 assert_eq!(units.byte(3), 5);
1037 assert_eq!(units.byte(4), 6);
1038 assert_eq!(units.code_unit(5), 3);
1039 assert_eq!(units.code_unit(6), 4);
1040 }
1041
1042 #[test]
1043 fn text_range_slices_by_code_unit() {
1044 let range = TextRange::new(1, 3);
1045 assert_eq!(range.text_inside("a😀b"), "😀");
1046 assert_eq!(range.text_before("a😀b"), "a");
1047 assert_eq!(range.text_after("a😀b"), "b");
1048 assert!(TextRange::EMPTY.is_collapsed());
1049 assert!(!TextRange::EMPTY.is_valid());
1050 }
1051
1052 #[test]
1053 fn text_box_start_and_end_follow_direction() {
1054 let ltr = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Ltr);
1055 let rtl = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Rtl);
1056 assert_eq!(ltr.start(), 1.0);
1057 assert_eq!(ltr.end(), 3.0);
1058 assert_eq!(rtl.start(), 3.0);
1059 assert_eq!(rtl.end(), 1.0);
1060 assert_eq!(ltr.to_rect(), Rect::from_ltrb(1.0, 2.0, 3.0, 4.0));
1061 }
1062
1063 #[test]
1064 fn pushed_styles_inherit_unset_fields() {
1065 let mut builder = ParagraphBuilder::new(ParagraphStyle {
1066 font_size: Some(20.0),
1067 ..ParagraphStyle::default()
1068 });
1069 builder.push_style(TextStyle {
1070 color: Some(Color::from_argb(255, 255, 0, 0)),
1071 ..TextStyle::default()
1072 });
1073 builder.add_text("x");
1074 builder.pop();
1075 builder.add_text("y");
1076 let styles: Vec<&valo::TextStyle> = builder.runs.iter().map(|(_, s)| s).collect();
1077 assert_eq!(styles[0].size, 20.0, "the child keeps the paragraph's size");
1078 assert_eq!(styles[0].color, Color::from_argb(255, 255, 0, 0).into());
1079 assert_eq!(styles[1].size, 20.0);
1080 assert_eq!(
1081 styles[1].color,
1082 valo::Color::BLACK,
1083 "popping restores the base"
1084 );
1085 }
1086
1087 #[test]
1088 fn start_and_end_alignment_resolve_against_direction() {
1089 assert_eq!(
1090 native_align(Some(TextAlign::Start), Some(TextDirection::Rtl)),
1091 valo::TextAlign::Right
1092 );
1093 assert_eq!(
1094 native_align(Some(TextAlign::End), None),
1095 valo::TextAlign::Right,
1096 "no direction reads as LTR"
1097 );
1098 }
1099
1100 #[test]
1101 fn unspecified_font_family_stays_unset_on_the_native_style() {
1102 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1103 builder.add_text("x");
1104 assert!(
1105 builder.runs[0].1.families.is_empty(),
1106 "dart:ui leaves fontFamily unset; the collection's default manager supplies the face"
1107 );
1108 }
1109}
1110
1111#[cfg(test)]
1112mod layout_tests {
1113 use super::*;
1116 use crate::Platform;
1117
1118 fn fonts() -> FontCollection {
1119 let mut fonts = FontCollection::new();
1120 fonts.add_source(valo_system_fonts::SystemFonts::load());
1121 fonts
1122 }
1123
1124 fn paragraph(text: &str, style: ParagraphStyle, width: f64) -> Paragraph {
1125 let mut builder = ParagraphBuilder::new(style);
1126 builder.push_style(TextStyle {
1127 font_size: Some(20.0),
1128 ..TextStyle::default()
1129 });
1130 builder.add_text(text);
1131 let mut paragraph = builder.build(&mut fonts());
1132 paragraph.layout(ParagraphConstraints::new(width));
1133 paragraph
1134 }
1135
1136 #[test]
1137 fn a_laid_out_paragraph_has_a_size_and_one_line() {
1138 let paragraph = paragraph("Hello", ParagraphStyle::new(), f64::INFINITY);
1139 assert_eq!(
1140 paragraph.width(),
1141 f64::INFINITY,
1142 "the layout width, as in Skia"
1143 );
1144 assert!(paragraph.longest_line() > 0.0);
1145 assert!(paragraph.height() > 0.0);
1146 assert_eq!(paragraph.number_of_lines(), 1);
1147 assert!(paragraph.alphabetic_baseline() > 0.0);
1148 assert!(paragraph.alphabetic_baseline() < paragraph.height());
1149 assert!(!paragraph.did_exceed_max_lines());
1150 assert_eq!(paragraph.max_intrinsic_width(), paragraph.longest_line());
1151 }
1152
1153 #[test]
1154 fn a_narrow_width_wraps_onto_more_lines() {
1155 let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1156 let narrow = paragraph(
1157 "one two three four",
1158 ParagraphStyle::new(),
1159 wide.longest_line() / 2.0,
1160 );
1161 assert!(narrow.number_of_lines() > 1);
1162 assert!(narrow.height() > wide.height());
1163 let metrics = narrow.compute_line_metrics();
1164 assert_eq!(metrics.len() as i32, narrow.number_of_lines());
1165 assert!(
1166 metrics.last().unwrap().hard_break,
1167 "the last line is a hard break"
1168 );
1169 assert!(!metrics[0].hard_break, "a wrapped line is a soft break");
1170 assert_eq!(metrics[1].line_number, 1);
1171 }
1172
1173 #[test]
1174 fn max_lines_with_an_ellipsis_truncates() {
1175 let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1176 let style = ParagraphStyle {
1177 max_lines: Some(1),
1178 ellipsis: Some("\u{2026}".to_owned()),
1179 ..ParagraphStyle::default()
1180 };
1181 let clipped = paragraph("one two three four", style, wide.longest_line() / 2.0);
1182 assert_eq!(clipped.number_of_lines(), 1);
1183 assert!(clipped.did_exceed_max_lines());
1184 }
1185
1186 #[test]
1187 fn positions_boxes_and_boundaries_use_code_units() {
1188 let paragraph = paragraph("ab cd", ParagraphStyle::new(), f64::INFINITY);
1189 let start = paragraph.get_position_for_offset(Offset::new(0.0, 1.0));
1190 assert_eq!(start.offset, 0);
1191 let end =
1192 paragraph.get_position_for_offset(Offset::new(paragraph.longest_line() + 50.0, 1.0));
1193 assert_eq!(end.offset, 5);
1194
1195 let boxes =
1196 paragraph.get_boxes_for_range(0, 2, BoxHeightStyle::Tight, BoxWidthStyle::Tight);
1197 assert_eq!(boxes.len(), 1);
1198 assert!(boxes[0].right > boxes[0].left);
1199 assert_eq!(boxes[0].direction, TextDirection::Ltr);
1200
1201 assert_eq!(
1202 paragraph.get_word_boundary(TextPosition::new(4)),
1203 TextRange::new(3, 5)
1204 );
1205 assert_eq!(
1206 paragraph.get_line_boundary(TextPosition::new(1)),
1207 TextRange::new(0, 5)
1208 );
1209 assert_eq!(paragraph.get_line_number_at(4), Some(0));
1210 assert_eq!(paragraph.get_line_number_at(5), None);
1211
1212 let glyph = paragraph.get_glyph_info_at(3).expect("the c glyph");
1213 assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(3, 4));
1214 assert!(glyph.grapheme_cluster_layout_bounds.width() > 0.0);
1215 assert!(paragraph.get_glyph_info_at(5).is_none());
1216 }
1217
1218 #[test]
1219 fn a_surrogate_pair_is_one_glyph_of_two_code_units() {
1220 let paragraph = paragraph("a😀b", ParagraphStyle::new(), f64::INFINITY);
1221 let glyph = paragraph.get_glyph_info_at(1).expect("the emoji");
1222 assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(1, 3));
1223 assert_eq!(
1224 paragraph
1225 .get_glyph_info_at(3)
1226 .unwrap()
1227 .grapheme_cluster_code_unit_range,
1228 TextRange::new(3, 4)
1229 );
1230 }
1231
1232 #[test]
1233 fn drawing_records_the_paragraph() {
1234 let paragraph = paragraph("Hi", ParagraphStyle::new(), f64::INFINITY);
1235 let mut canvas = Canvas::new();
1236 canvas.draw_paragraph(¶graph, Offset::new(10.0, 10.0));
1237 let picture = canvas.build();
1238 assert!(!picture.ops().is_empty());
1239 }
1240
1241 #[test]
1242 fn unspecified_family_at_black_weight_covers_latin() {
1243 let mut fonts = FontCollection::new();
1244 crate::set_default_font_manager(
1245 &mut fonts,
1246 Box::new(crate::SystemFontSource::platform()),
1247 &crate::InertPlatform.default_font_families(),
1248 );
1249 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1250 builder.push_style(TextStyle {
1251 font_size: Some(10.2),
1252 font_weight: Some(FontWeight::W900),
1253 ..TextStyle::default()
1254 });
1255 builder.add_text("DEBUG");
1256 let mut paragraph = builder.build(&mut fonts);
1257 paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1258 assert!(paragraph.longest_line() > 0.0);
1259 assert!(
1260 fonts.take_unanswered().is_empty(),
1261 "the engine default family covers Latin at weight 900"
1262 );
1263 for line in paragraph.inner.lines() {
1264 for run in &line.runs {
1265 assert!(
1266 run.glyphs.iter().all(|glyph| glyph.id != 0),
1267 "no .notdef tofu"
1268 );
1269 }
1270 }
1271 }
1272}