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_placeholder_alignment(alignment: PlaceholderAlignment) -> valo::PlaceholderAlignment {
502 match alignment {
503 PlaceholderAlignment::Baseline => valo::PlaceholderAlignment::Baseline,
504 PlaceholderAlignment::AboveBaseline => valo::PlaceholderAlignment::AboveBaseline,
505 PlaceholderAlignment::BelowBaseline => valo::PlaceholderAlignment::BelowBaseline,
506 PlaceholderAlignment::Top => valo::PlaceholderAlignment::Top,
507 PlaceholderAlignment::Bottom => valo::PlaceholderAlignment::Bottom,
508 PlaceholderAlignment::Middle => valo::PlaceholderAlignment::Middle,
509 }
510}
511
512fn native_baseline(baseline: TextBaseline) -> valo::TextBaseline {
513 match baseline {
514 TextBaseline::Alphabetic => valo::TextBaseline::Alphabetic,
515 TextBaseline::Ideographic => valo::TextBaseline::Ideographic,
516 }
517}
518
519fn native_direction(direction: TextDirection) -> valo::TextDirection {
520 match direction {
521 TextDirection::Ltr => valo::TextDirection::Ltr,
522 TextDirection::Rtl => valo::TextDirection::Rtl,
523 }
524}
525
526pub struct ParagraphBuilder {
540 paragraph_style: ParagraphStyle,
541 style_stack: Vec<TextStyle>,
542 pieces: Vec<Piece>,
543 placeholder_scales: Vec<f64>,
544}
545
546enum Piece {
549 Text(String, valo::TextStyle),
550 Placeholder(valo::PlaceholderStyle, valo::TextStyle),
551}
552
553impl ParagraphBuilder {
554 pub fn new(style: ParagraphStyle) -> ParagraphBuilder {
556 let base = style.base_text_style();
557 ParagraphBuilder {
558 paragraph_style: style,
559 style_stack: vec![base],
560 pieces: Vec::new(),
561 placeholder_scales: Vec::new(),
562 }
563 }
564
565 pub fn placeholder_count(&self) -> i32 {
567 self.placeholder_scales.len() as i32
568 }
569
570 pub fn placeholder_scales(&self) -> &[f64] {
572 &self.placeholder_scales
573 }
574
575 pub fn push_style(&mut self, style: TextStyle) {
579 let current = self
580 .style_stack
581 .last()
582 .expect("the base style is never popped");
583 let merged = current.merged_with(&style);
584 self.style_stack.push(merged);
585 }
586
587 pub fn pop(&mut self) {
593 if self.style_stack.len() > 1 {
594 self.style_stack.pop();
595 }
596 }
597
598 pub fn add_text(&mut self, text: &str) {
602 let style = self.current_style();
603 self.pieces.push(Piece::Text(text.to_owned(), style));
604 }
605
606 pub fn add_placeholder(
657 &mut self,
658 width: f64,
659 height: f64,
660 alignment: PlaceholderAlignment,
661 scale: f64,
662 baseline_offset: Option<f64>,
663 baseline: Option<TextBaseline>,
664 ) {
665 debug_assert!(
667 !matches!(
668 alignment,
669 PlaceholderAlignment::AboveBaseline
670 | PlaceholderAlignment::BelowBaseline
671 | PlaceholderAlignment::Baseline
672 ) || baseline.is_some()
673 );
674 let baseline_offset = baseline_offset.unwrap_or(height);
677 let placeholder = valo::PlaceholderStyle {
678 width: (width * scale) as f32,
679 height: (height * scale) as f32,
680 alignment: native_placeholder_alignment(alignment),
681 baseline: native_baseline(baseline.unwrap_or(TextBaseline::Alphabetic)),
682 baseline_offset: (baseline_offset * scale) as f32,
683 };
684 let style = self.current_style();
685 self.pieces.push(Piece::Placeholder(placeholder, style));
686 self.placeholder_scales.push(scale);
687 }
688
689 fn current_style(&self) -> valo::TextStyle {
691 self.style_stack
692 .last()
693 .expect("the base style is never popped")
694 .to_native()
695 }
696
697 pub fn build(self, fonts: &mut FontCollection) -> Paragraph {
703 let direction = self
704 .paragraph_style
705 .text_direction
706 .unwrap_or(TextDirection::Ltr);
707 let mut builder = valo::ParagraphBuilder::new(fonts);
708 builder.style(self.paragraph_style.to_native());
709 for piece in &self.pieces {
710 match piece {
711 Piece::Text(text, style) => builder.add_text(text, style),
712 Piece::Placeholder(placeholder, style) => {
713 builder.add_placeholder(*placeholder, style)
714 }
715 };
716 }
717 let inner = builder.build();
718 Paragraph {
719 code_units: CodeUnits::of(inner.text()),
720 inner,
721 direction,
722 layout_width: 0.0,
723 disposed: false,
724 }
725 }
726}
727
728#[derive(Clone, Debug)]
730struct CodeUnits {
731 byte_of: Vec<usize>,
733 len: usize,
735}
736
737impl CodeUnits {
738 fn of(text: &str) -> CodeUnits {
739 let mut byte_of = Vec::with_capacity(text.len() + 1);
740 for (byte, ch) in text.char_indices() {
741 byte_of.push(byte);
742 if ch.len_utf16() == 2 {
743 byte_of.push(byte);
745 }
746 }
747 byte_of.push(text.len());
748 let len = byte_of.len() - 1;
749 CodeUnits { byte_of, len }
750 }
751
752 fn byte(&self, code_unit: i32) -> usize {
753 let clamped = code_unit.clamp(0, self.len as i32) as usize;
754 self.byte_of[clamped]
755 }
756
757 fn code_unit(&self, byte: usize) -> i32 {
759 let first_at_or_after = self.byte_of.partition_point(|candidate| *candidate < byte);
760 match self.byte_of.get(first_at_or_after) {
761 Some(candidate) if *candidate == byte => first_at_or_after as i32,
762 _ => first_at_or_after.saturating_sub(1) as i32,
763 }
764 }
765}
766
767fn byte_of_code_unit(text: &str, code_unit: usize) -> usize {
769 CodeUnits::of(text).byte(code_unit as i32)
770}
771
772pub struct Paragraph {
781 inner: valo::Paragraph,
782 code_units: CodeUnits,
783 direction: TextDirection,
784 layout_width: f64,
786 disposed: bool,
787}
788
789impl Paragraph {
790 pub fn width(&self) -> f64 {
796 self.layout_width
797 }
798
799 pub fn height(&self) -> f64 {
803 f64::from(self.inner.height())
804 }
805
806 pub fn longest_line(&self) -> f64 {
809 f64::from(self.inner.longest_line())
810 }
811
812 pub fn min_intrinsic_width(&self) -> f64 {
815 f64::from(self.inner.min_intrinsic_width())
816 }
817
818 pub fn max_intrinsic_width(&self) -> f64 {
821 f64::from(self.inner.max_intrinsic_width())
822 }
823
824 pub fn alphabetic_baseline(&self) -> f64 {
827 self.inner
828 .lines()
829 .first()
830 .map_or(0.0, |line| f64::from(line.baseline))
831 }
832
833 pub fn ideographic_baseline(&self) -> f64 {
838 self.inner
839 .lines()
840 .first()
841 .map_or(0.0, |line| f64::from(line.baseline + line.descent))
842 }
843
844 pub fn did_exceed_max_lines(&self) -> bool {
848 self.inner.truncated()
849 }
850
851 pub fn layout(&mut self, constraints: ParagraphConstraints) {
855 debug_assert!(!self.disposed);
856 self.layout_width = constraints.width;
857 self.inner.layout(constraints.width as f32);
858 }
859
860 pub fn get_boxes_for_range(
865 &self,
866 start: i32,
867 end: i32,
868 box_height_style: BoxHeightStyle,
869 box_width_style: BoxWidthStyle,
870 ) -> Vec<TextBox> {
871 let _ = (box_height_style, box_width_style);
872 let range = self.code_units.byte(start)..self.code_units.byte(end);
873 self.inner
874 .rects_for_range(range)
875 .into_iter()
876 .map(|rect| text_box(rect, self.direction))
877 .collect()
878 }
879
880 pub fn get_boxes_for_placeholders(&self) -> Vec<TextBox> {
888 self.inner
889 .placeholder_boxes()
890 .into_iter()
891 .map(|placeholder| {
892 let direction = match placeholder.direction {
893 valo::TextDirection::Ltr => TextDirection::Ltr,
894 valo::TextDirection::Rtl => TextDirection::Rtl,
895 };
896 text_box(placeholder.rect, direction)
897 })
898 .collect()
899 }
900
901 pub fn get_position_for_offset(&self, offset: Offset) -> TextPosition {
903 let position = self.inner.glyph_position_at(offset.into());
904 TextPosition::with_affinity(
905 self.code_units.code_unit(position.offset),
906 if position.downstream {
907 TextAffinity::Downstream
908 } else {
909 TextAffinity::Upstream
910 },
911 )
912 }
913
914 pub fn get_closest_glyph_info_for_offset(&self, offset: Offset) -> Option<GlyphInfo> {
918 let position = self.inner.glyph_position_at(offset.into());
919 let anchor = if position.downstream {
920 position.offset
921 } else {
922 position.offset.saturating_sub(1)
923 };
924 self.glyph_info_at_byte(anchor)
925 }
926
927 pub fn get_glyph_info_at(&self, code_unit_offset: i32) -> Option<GlyphInfo> {
930 if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
931 return None;
932 }
933 self.glyph_info_at_byte(self.code_units.byte(code_unit_offset))
934 }
935
936 fn glyph_info_at_byte(&self, byte: usize) -> Option<GlyphInfo> {
939 for line in self.inner.lines() {
940 if !line.range.contains(&byte) {
941 continue;
942 }
943 let top = line.baseline - line.ascent;
944 let bottom = line.baseline + line.descent;
945 let cluster_starts = cluster_starts(line);
946 for run in &line.runs {
947 for glyph in &run.glyphs {
948 let cluster = cluster_range(&cluster_starts, glyph.cluster, line.range.end);
949 if !cluster.contains(&byte) {
950 continue;
951 }
952 let (left, right) = if glyph.advance >= 0.0 {
953 (glyph.x, glyph.x + glyph.advance)
954 } else {
955 (glyph.x + glyph.advance, glyph.x)
956 };
957 return Some(GlyphInfo {
958 grapheme_cluster_layout_bounds: Rect::from_ltrb(
959 f64::from(left),
960 f64::from(top),
961 f64::from(right),
962 f64::from(bottom),
963 ),
964 grapheme_cluster_code_unit_range: TextRange::new(
965 self.code_units.code_unit(cluster.start),
966 self.code_units.code_unit(cluster.end),
967 ),
968 writing_direction: if run.rtl {
969 TextDirection::Rtl
970 } else {
971 TextDirection::Ltr
972 },
973 });
974 }
975 }
976 }
977 None
978 }
979
980 pub fn get_word_boundary(&self, position: TextPosition) -> TextRange {
982 let range = self
983 .inner
984 .word_boundary(self.code_units.byte(position.offset));
985 self.text_range(range)
986 }
987
988 pub fn get_line_boundary(&self, position: TextPosition) -> TextRange {
992 let byte = self.code_units.byte(position.offset);
993 let Some(line) = self.line_containing(byte, position.affinity) else {
994 return TextRange::EMPTY;
995 };
996 let text = self.inner.text();
997 let mut end = line.range.end;
998 if end > line.range.start && text.as_bytes().get(end - 1) == Some(&b'\n') {
999 end -= 1;
1000 }
1001 self.text_range(line.range.start..end)
1002 }
1003
1004 fn line_containing(&self, byte: usize, affinity: TextAffinity) -> Option<&valo::Line> {
1005 let lines = self.inner.lines();
1006 let index = lines.iter().position(|line| {
1007 let starts_next_line = byte == line.range.start && affinity == TextAffinity::Upstream;
1008 (line.range.contains(&byte) && !starts_next_line) || byte == line.range.end
1009 });
1010 index.and_then(|index| lines.get(index)).or_else(|| {
1011 let last = lines.last()?;
1012 (byte >= last.range.end).then_some(last)
1013 })
1014 }
1015
1016 fn text_range(&self, range: Range<usize>) -> TextRange {
1017 TextRange::new(
1018 self.code_units.code_unit(range.start),
1019 self.code_units.code_unit(range.end),
1020 )
1021 }
1022
1023 pub fn compute_line_metrics(&self) -> Vec<LineMetrics> {
1026 let lines = self.inner.lines();
1027 let text = self.inner.text();
1028 lines
1029 .iter()
1030 .enumerate()
1031 .map(|(index, line)| {
1032 let is_last = index + 1 == lines.len();
1033 let ends_with_newline = line.range.end > line.range.start
1034 && text.as_bytes().get(line.range.end - 1) == Some(&b'\n');
1035 LineMetrics {
1036 hard_break: is_last || ends_with_newline,
1037 ascent: f64::from(line.ascent),
1038 descent: f64::from(line.descent),
1039 unscaled_ascent: f64::from(line.ascent),
1040 height: f64::from(line.ascent + line.descent),
1041 width: f64::from(line.width),
1042 left: f64::from(line.left),
1043 baseline: f64::from(line.baseline),
1044 line_number: index as i32,
1045 }
1046 })
1047 .collect()
1048 }
1049
1050 pub fn get_line_metrics_at(&self, line_number: i32) -> Option<LineMetrics> {
1053 if line_number < 0 {
1054 return None;
1055 }
1056 self.compute_line_metrics()
1057 .into_iter()
1058 .nth(line_number as usize)
1059 }
1060
1061 pub fn number_of_lines(&self) -> i32 {
1066 self.inner.lines().len() as i32
1067 }
1068
1069 pub fn get_line_number_at(&self, code_unit_offset: i32) -> Option<i32> {
1072 if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
1073 return None;
1074 }
1075 let byte = self.code_units.byte(code_unit_offset);
1076 self.inner
1077 .lines()
1078 .iter()
1079 .position(|line| line.range.contains(&byte))
1080 .map(|index| index as i32)
1081 }
1082
1083 pub fn dispose(&mut self) {
1086 debug_assert!(!self.disposed);
1087 self.disposed = true;
1088 }
1089
1090 pub fn debug_disposed(&self) -> bool {
1092 self.disposed
1093 }
1094
1095 pub(crate) fn native(&self) -> &valo::Paragraph {
1097 &self.inner
1098 }
1099}
1100
1101fn text_box(rect: valo::Rect, direction: TextDirection) -> TextBox {
1102 TextBox::from_ltrbd(
1103 f64::from(rect.x),
1104 f64::from(rect.y),
1105 f64::from(rect.x + rect.width),
1106 f64::from(rect.y + rect.height),
1107 direction,
1108 )
1109}
1110
1111fn cluster_starts(line: &valo::Line) -> Vec<usize> {
1114 let mut starts: Vec<usize> = line
1115 .runs
1116 .iter()
1117 .flat_map(|run| run.glyphs.iter().map(|glyph| glyph.cluster))
1118 .collect();
1119 starts.sort_unstable();
1120 starts.dedup();
1121 starts
1122}
1123
1124fn cluster_range(cluster_starts: &[usize], start: usize, line_end: usize) -> Range<usize> {
1125 let next = cluster_starts
1126 .iter()
1127 .find(|candidate| **candidate > start)
1128 .copied()
1129 .unwrap_or(line_end);
1130 start..next.max(start + 1)
1131}
1132
1133impl fmt::Debug for Paragraph {
1134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1135 f.debug_struct("Paragraph")
1136 .field("text", &self.inner.text())
1137 .field("width", &self.width())
1138 .field("height", &self.height())
1139 .finish()
1140 }
1141}
1142
1143pub trait CanvasText {
1145 fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset);
1149}
1150
1151impl CanvasText for Canvas {
1152 fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset) {
1153 valo::DrawParagraphExt::draw_paragraph(self, paragraph.native(), offset);
1154 }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160
1161 #[test]
1162 fn code_units_map_surrogate_pairs_to_one_character() {
1163 let units = CodeUnits::of("a😀b");
1164 assert_eq!(units.len, 4);
1165 assert_eq!(units.byte(0), 0);
1166 assert_eq!(units.byte(1), 1);
1167 assert_eq!(
1168 units.byte(2),
1169 1,
1170 "the second half of the pair maps to the same byte"
1171 );
1172 assert_eq!(units.byte(3), 5);
1173 assert_eq!(units.byte(4), 6);
1174 assert_eq!(units.code_unit(5), 3);
1175 assert_eq!(units.code_unit(6), 4);
1176 }
1177
1178 #[test]
1179 fn text_range_slices_by_code_unit() {
1180 let range = TextRange::new(1, 3);
1181 assert_eq!(range.text_inside("a😀b"), "😀");
1182 assert_eq!(range.text_before("a😀b"), "a");
1183 assert_eq!(range.text_after("a😀b"), "b");
1184 assert!(TextRange::EMPTY.is_collapsed());
1185 assert!(!TextRange::EMPTY.is_valid());
1186 }
1187
1188 #[test]
1189 fn text_box_start_and_end_follow_direction() {
1190 let ltr = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Ltr);
1191 let rtl = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Rtl);
1192 assert_eq!(ltr.start(), 1.0);
1193 assert_eq!(ltr.end(), 3.0);
1194 assert_eq!(rtl.start(), 3.0);
1195 assert_eq!(rtl.end(), 1.0);
1196 assert_eq!(ltr.to_rect(), Rect::from_ltrb(1.0, 2.0, 3.0, 4.0));
1197 }
1198
1199 fn text_styles(builder: &ParagraphBuilder) -> Vec<&valo::TextStyle> {
1200 builder
1201 .pieces
1202 .iter()
1203 .filter_map(|piece| match piece {
1204 Piece::Text(_, style) => Some(style),
1205 Piece::Placeholder(..) => None,
1206 })
1207 .collect()
1208 }
1209
1210 #[test]
1211 fn placeholders_are_counted_with_their_scales() {
1212 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1213 builder.add_text("a");
1214 builder.add_placeholder(10.0, 20.0, PlaceholderAlignment::Middle, 2.0, None, None);
1215 builder.add_placeholder(5.0, 5.0, PlaceholderAlignment::Bottom, 1.0, None, None);
1216 assert_eq!(builder.placeholder_count(), 2);
1217 assert_eq!(builder.placeholder_scales(), [2.0, 1.0]);
1218 }
1219
1220 #[test]
1221 fn pushed_styles_inherit_unset_fields() {
1222 let mut builder = ParagraphBuilder::new(ParagraphStyle {
1223 font_size: Some(20.0),
1224 ..ParagraphStyle::default()
1225 });
1226 builder.push_style(TextStyle {
1227 color: Some(Color::from_argb(255, 255, 0, 0)),
1228 ..TextStyle::default()
1229 });
1230 builder.add_text("x");
1231 builder.pop();
1232 builder.add_text("y");
1233 let styles = text_styles(&builder);
1234 assert_eq!(styles[0].size, 20.0, "the child keeps the paragraph's size");
1235 assert_eq!(styles[0].color, Color::from_argb(255, 255, 0, 0).into());
1236 assert_eq!(styles[1].size, 20.0);
1237 assert_eq!(
1238 styles[1].color,
1239 valo::Color::BLACK,
1240 "popping restores the base"
1241 );
1242 }
1243
1244 #[test]
1245 fn start_and_end_alignment_resolve_against_direction() {
1246 assert_eq!(
1247 native_align(Some(TextAlign::Start), Some(TextDirection::Rtl)),
1248 valo::TextAlign::Right
1249 );
1250 assert_eq!(
1251 native_align(Some(TextAlign::End), None),
1252 valo::TextAlign::Right,
1253 "no direction reads as LTR"
1254 );
1255 }
1256
1257 #[test]
1258 fn unspecified_font_family_stays_unset_on_the_native_style() {
1259 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1260 builder.add_text("x");
1261 assert!(
1262 text_styles(&builder)[0].families.is_empty(),
1263 "dart:ui leaves fontFamily unset; the collection's default manager supplies the face"
1264 );
1265 }
1266}
1267
1268#[cfg(test)]
1269mod layout_tests {
1270 use super::*;
1273 use crate::Platform;
1274
1275 fn fonts() -> FontCollection {
1276 let mut fonts = FontCollection::new();
1277 fonts.add_source(valo_system_fonts::SystemFonts::load());
1278 fonts
1279 }
1280
1281 fn paragraph(text: &str, style: ParagraphStyle, width: f64) -> Paragraph {
1282 let mut builder = ParagraphBuilder::new(style);
1283 builder.push_style(TextStyle {
1284 font_size: Some(20.0),
1285 ..TextStyle::default()
1286 });
1287 builder.add_text(text);
1288 let mut paragraph = builder.build(&mut fonts());
1289 paragraph.layout(ParagraphConstraints::new(width));
1290 paragraph
1291 }
1292
1293 #[test]
1294 fn a_laid_out_paragraph_has_a_size_and_one_line() {
1295 let paragraph = paragraph("Hello", ParagraphStyle::new(), f64::INFINITY);
1296 assert_eq!(
1297 paragraph.width(),
1298 f64::INFINITY,
1299 "the layout width, as in Skia"
1300 );
1301 assert!(paragraph.longest_line() > 0.0);
1302 assert!(paragraph.height() > 0.0);
1303 assert_eq!(paragraph.number_of_lines(), 1);
1304 assert!(paragraph.alphabetic_baseline() > 0.0);
1305 assert!(paragraph.alphabetic_baseline() < paragraph.height());
1306 assert!(!paragraph.did_exceed_max_lines());
1307 assert_eq!(paragraph.max_intrinsic_width(), paragraph.longest_line());
1308 }
1309
1310 #[test]
1311 fn a_narrow_width_wraps_onto_more_lines() {
1312 let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1313 let narrow = paragraph(
1314 "one two three four",
1315 ParagraphStyle::new(),
1316 wide.longest_line() / 2.0,
1317 );
1318 assert!(narrow.number_of_lines() > 1);
1319 assert!(narrow.height() > wide.height());
1320 let metrics = narrow.compute_line_metrics();
1321 assert_eq!(metrics.len() as i32, narrow.number_of_lines());
1322 assert!(
1323 metrics.last().unwrap().hard_break,
1324 "the last line is a hard break"
1325 );
1326 assert!(!metrics[0].hard_break, "a wrapped line is a soft break");
1327 assert_eq!(metrics[1].line_number, 1);
1328 }
1329
1330 #[test]
1331 fn max_lines_with_an_ellipsis_truncates() {
1332 let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1333 let style = ParagraphStyle {
1334 max_lines: Some(1),
1335 ellipsis: Some("\u{2026}".to_owned()),
1336 ..ParagraphStyle::default()
1337 };
1338 let clipped = paragraph("one two three four", style, wide.longest_line() / 2.0);
1339 assert_eq!(clipped.number_of_lines(), 1);
1340 assert!(clipped.did_exceed_max_lines());
1341 }
1342
1343 #[test]
1344 fn positions_boxes_and_boundaries_use_code_units() {
1345 let paragraph = paragraph("ab cd", ParagraphStyle::new(), f64::INFINITY);
1346 let start = paragraph.get_position_for_offset(Offset::new(0.0, 1.0));
1347 assert_eq!(start.offset, 0);
1348 let end =
1349 paragraph.get_position_for_offset(Offset::new(paragraph.longest_line() + 50.0, 1.0));
1350 assert_eq!(end.offset, 5);
1351
1352 let boxes =
1353 paragraph.get_boxes_for_range(0, 2, BoxHeightStyle::Tight, BoxWidthStyle::Tight);
1354 assert_eq!(boxes.len(), 1);
1355 assert!(boxes[0].right > boxes[0].left);
1356 assert_eq!(boxes[0].direction, TextDirection::Ltr);
1357
1358 assert_eq!(
1359 paragraph.get_word_boundary(TextPosition::new(4)),
1360 TextRange::new(3, 5)
1361 );
1362 assert_eq!(
1363 paragraph.get_line_boundary(TextPosition::new(1)),
1364 TextRange::new(0, 5)
1365 );
1366 assert_eq!(paragraph.get_line_number_at(4), Some(0));
1367 assert_eq!(paragraph.get_line_number_at(5), None);
1368
1369 let glyph = paragraph.get_glyph_info_at(3).expect("the c glyph");
1370 assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(3, 4));
1371 assert!(glyph.grapheme_cluster_layout_bounds.width() > 0.0);
1372 assert!(paragraph.get_glyph_info_at(5).is_none());
1373 }
1374
1375 #[test]
1376 fn a_surrogate_pair_is_one_glyph_of_two_code_units() {
1377 let paragraph = paragraph("a😀b", ParagraphStyle::new(), f64::INFINITY);
1378 let glyph = paragraph.get_glyph_info_at(1).expect("the emoji");
1379 assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(1, 3));
1380 assert_eq!(
1381 paragraph
1382 .get_glyph_info_at(3)
1383 .unwrap()
1384 .grapheme_cluster_code_unit_range,
1385 TextRange::new(3, 4)
1386 );
1387 }
1388
1389 #[test]
1392 fn the_closest_glyph_is_the_one_under_the_point() {
1393 let paragraph = paragraph("abc", ParagraphStyle::new(), f64::INFINITY);
1394 let b = paragraph.get_glyph_info_at(1).expect("the b glyph");
1395 let bounds = b.grapheme_cluster_layout_bounds;
1396 let y = bounds.center().dy();
1397 for x in [bounds.left + 0.5, bounds.right - 0.5] {
1398 let closest = paragraph
1399 .get_closest_glyph_info_for_offset(Offset::new(x, y))
1400 .expect("a glyph");
1401 assert_eq!(
1402 closest.grapheme_cluster_code_unit_range,
1403 TextRange::new(1, 2)
1404 );
1405 }
1406 let past_the_end = paragraph
1407 .get_closest_glyph_info_for_offset(Offset::new(1000.0, y))
1408 .expect("a glyph");
1409 assert_eq!(
1410 past_the_end.grapheme_cluster_code_unit_range,
1411 TextRange::new(2, 3)
1412 );
1413 }
1414
1415 #[test]
1416 fn placeholder_boxes_come_in_the_order_added_at_their_scaled_size() {
1417 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1418 builder.push_style(TextStyle {
1419 font_size: Some(20.0),
1420 ..TextStyle::default()
1421 });
1422 builder.add_text("ab");
1423 builder.add_placeholder(
1424 30.0,
1425 50.0,
1426 PlaceholderAlignment::Baseline,
1427 1.0,
1428 Some(40.0),
1429 Some(TextBaseline::Alphabetic),
1430 );
1431 builder.add_text("cd");
1432 builder.add_placeholder(10.0, 10.0, PlaceholderAlignment::Bottom, 2.0, None, None);
1433 let mut paragraph = builder.build(&mut fonts());
1434 paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1435
1436 let boxes = paragraph.get_boxes_for_placeholders();
1437 assert_eq!(boxes.len(), 2);
1438 let baseline = paragraph.alphabetic_baseline();
1439 let (first, second) = (boxes[0], boxes[1]);
1440 assert!((first.right - first.left - 30.0).abs() < 1e-3);
1441 assert!(
1442 (first.top - (baseline - 40.0)).abs() < 1e-3,
1443 "40 above the baseline"
1444 );
1445 assert!(
1446 (first.bottom - (baseline + 10.0)).abs() < 1e-3,
1447 "10 below it"
1448 );
1449 assert!(
1450 (second.right - second.left - 20.0).abs() < 1e-3,
1451 "scaled by 2"
1452 );
1453 assert!((second.bottom - second.top - 20.0).abs() < 1e-3);
1454 assert!(second.left > first.right, "after the text between them");
1455 assert_eq!(
1456 paragraph.get_line_boundary(TextPosition::new(0)),
1457 TextRange::new(0, 6),
1458 "each placeholder is one code unit"
1459 );
1460 }
1461
1462 #[test]
1463 fn drawing_records_the_paragraph() {
1464 let paragraph = paragraph("Hi", ParagraphStyle::new(), f64::INFINITY);
1465 let mut canvas = Canvas::new();
1466 canvas.draw_paragraph(¶graph, Offset::new(10.0, 10.0));
1467 let picture = canvas.build();
1468 assert!(!picture.ops().is_empty());
1469 }
1470
1471 #[test]
1472 fn unspecified_family_at_black_weight_covers_latin() {
1473 let mut fonts = FontCollection::new();
1474 crate::set_default_font_manager(
1475 &mut fonts,
1476 Box::new(crate::SystemFontSource::platform()),
1477 &crate::InertPlatform.default_font_families(),
1478 );
1479 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1480 builder.push_style(TextStyle {
1481 font_size: Some(10.2),
1482 font_weight: Some(FontWeight::W900),
1483 ..TextStyle::default()
1484 });
1485 builder.add_text("DEBUG");
1486 let mut paragraph = builder.build(&mut fonts);
1487 paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1488 assert!(paragraph.longest_line() > 0.0);
1489 assert!(
1490 fonts.take_unanswered().is_empty(),
1491 "the engine default family covers Latin at weight 900"
1492 );
1493 for line in paragraph.inner.lines() {
1494 for run in &line.runs {
1495 assert!(
1496 run.glyphs.iter().all(|glyph| glyph.id != 0),
1497 "no .notdef tofu"
1498 );
1499 }
1500 }
1501 }
1502}