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 style.foreground = self.foreground.clone();
383 style.background = self.background.clone();
384 if let Some(letter_spacing) = self.letter_spacing {
385 style.letter_spacing = letter_spacing as f32;
386 }
387 if let Some(word_spacing) = self.word_spacing {
388 style.word_spacing = word_spacing as f32;
389 }
390 style.height = match self.height {
391 Some(height) if height != crate::text::K_TEXT_HEIGHT_NONE => Some(height as f32),
392 _ => None,
393 };
394 style.decoration = native_decoration(
395 self.decoration,
396 self.decoration_color,
397 self.decoration_thickness,
398 );
399 if let Some(shadows) = &self.shadows {
400 style.shadows = shadows
401 .iter()
402 .map(|shadow| valo::Shadow {
403 color: shadow.color.into(),
404 offset: shadow.offset.into(),
405 blur: shadow.blur_sigma() as f32,
406 })
407 .collect();
408 }
409 style
410 }
411}
412
413fn native_decoration(
415 decoration: Option<TextDecoration>,
416 color: Option<Color>,
417 thickness: Option<f64>,
418) -> Option<valo::Decoration> {
419 let decoration = decoration?;
420 let kind = if decoration.contains(TextDecoration::UNDERLINE) {
421 valo::DecorationKind::Underline
422 } else if decoration.contains(TextDecoration::OVERLINE) {
423 valo::DecorationKind::Overline
424 } else if decoration.contains(TextDecoration::LINE_THROUGH) {
425 valo::DecorationKind::LineThrough
426 } else {
427 return None;
428 };
429 Some(valo::Decoration {
430 kind,
431 color: color.map(Into::into),
432 thickness: thickness.unwrap_or(1.0) as f32,
433 })
434}
435
436#[derive(Clone, Debug, Default, PartialEq)]
442pub struct ParagraphStyle {
443 pub text_align: Option<TextAlign>,
444 pub text_direction: Option<TextDirection>,
445 pub max_lines: Option<i32>,
446 pub font_family: Option<String>,
447 pub font_size: Option<f64>,
448 pub height: Option<f64>,
449 pub text_height_behavior: Option<TextHeightBehavior>,
450 pub font_weight: Option<FontWeight>,
451 pub font_style: Option<FontStyle>,
452 pub ellipsis: Option<String>,
453}
454
455impl ParagraphStyle {
456 pub fn new() -> ParagraphStyle {
458 ParagraphStyle::default()
459 }
460
461 fn base_text_style(&self) -> TextStyle {
463 TextStyle {
464 font_family: self.font_family.clone(),
465 font_size: self.font_size,
466 height: self.height,
467 font_weight: self.font_weight,
468 font_style: self.font_style,
469 ..TextStyle::default()
470 }
471 }
472
473 fn to_native(&self) -> valo::ParagraphStyle {
474 valo::ParagraphStyle {
475 align: native_align(self.text_align, self.text_direction),
476 direction: self.text_direction.map(native_direction),
477 preserve_trailing_whitespace: false,
478 max_lines: self.max_lines.map(|max_lines| max_lines as u32),
479 ellipsis: self.ellipsis.clone(),
480 }
481 }
482}
483
484fn native_align(align: Option<TextAlign>, direction: Option<TextDirection>) -> valo::TextAlign {
487 match align {
488 None | Some(TextAlign::Left) => valo::TextAlign::Left,
489 Some(TextAlign::Right) => valo::TextAlign::Right,
490 Some(TextAlign::Center) => valo::TextAlign::Center,
491 Some(TextAlign::Justify) => valo::TextAlign::Justify,
492 Some(TextAlign::Start) => match direction {
493 Some(TextDirection::Rtl) => valo::TextAlign::Right,
494 _ => valo::TextAlign::Left,
495 },
496 Some(TextAlign::End) => match direction {
497 Some(TextDirection::Rtl) => valo::TextAlign::Left,
498 _ => valo::TextAlign::Right,
499 },
500 }
501}
502
503fn native_placeholder_alignment(alignment: PlaceholderAlignment) -> valo::PlaceholderAlignment {
504 match alignment {
505 PlaceholderAlignment::Baseline => valo::PlaceholderAlignment::Baseline,
506 PlaceholderAlignment::AboveBaseline => valo::PlaceholderAlignment::AboveBaseline,
507 PlaceholderAlignment::BelowBaseline => valo::PlaceholderAlignment::BelowBaseline,
508 PlaceholderAlignment::Top => valo::PlaceholderAlignment::Top,
509 PlaceholderAlignment::Bottom => valo::PlaceholderAlignment::Bottom,
510 PlaceholderAlignment::Middle => valo::PlaceholderAlignment::Middle,
511 }
512}
513
514fn native_baseline(baseline: TextBaseline) -> valo::TextBaseline {
515 match baseline {
516 TextBaseline::Alphabetic => valo::TextBaseline::Alphabetic,
517 TextBaseline::Ideographic => valo::TextBaseline::Ideographic,
518 }
519}
520
521fn native_direction(direction: TextDirection) -> valo::TextDirection {
522 match direction {
523 TextDirection::Ltr => valo::TextDirection::Ltr,
524 TextDirection::Rtl => valo::TextDirection::Rtl,
525 }
526}
527
528pub struct ParagraphBuilder {
542 paragraph_style: ParagraphStyle,
543 style_stack: Vec<TextStyle>,
544 pieces: Vec<Piece>,
545 placeholder_scales: Vec<f64>,
546}
547
548enum Piece {
551 Text(String, valo::TextStyle),
552 Placeholder(valo::PlaceholderStyle, valo::TextStyle),
553}
554
555impl ParagraphBuilder {
556 pub fn new(style: ParagraphStyle) -> ParagraphBuilder {
558 let base = style.base_text_style();
559 ParagraphBuilder {
560 paragraph_style: style,
561 style_stack: vec![base],
562 pieces: Vec::new(),
563 placeholder_scales: Vec::new(),
564 }
565 }
566
567 pub fn placeholder_count(&self) -> i32 {
569 self.placeholder_scales.len() as i32
570 }
571
572 pub fn placeholder_scales(&self) -> &[f64] {
574 &self.placeholder_scales
575 }
576
577 pub fn push_style(&mut self, style: TextStyle) {
581 let current = self
582 .style_stack
583 .last()
584 .expect("the base style is never popped");
585 let merged = current.merged_with(&style);
586 self.style_stack.push(merged);
587 }
588
589 pub fn pop(&mut self) {
595 if self.style_stack.len() > 1 {
596 self.style_stack.pop();
597 }
598 }
599
600 pub fn add_text(&mut self, text: &str) {
604 let style = self.current_style();
605 self.pieces.push(Piece::Text(text.to_owned(), style));
606 }
607
608 pub fn add_placeholder(
659 &mut self,
660 width: f64,
661 height: f64,
662 alignment: PlaceholderAlignment,
663 scale: f64,
664 baseline_offset: Option<f64>,
665 baseline: Option<TextBaseline>,
666 ) {
667 debug_assert!(
669 !matches!(
670 alignment,
671 PlaceholderAlignment::AboveBaseline
672 | PlaceholderAlignment::BelowBaseline
673 | PlaceholderAlignment::Baseline
674 ) || baseline.is_some()
675 );
676 let baseline_offset = baseline_offset.unwrap_or(height);
679 let placeholder = valo::PlaceholderStyle {
680 width: (width * scale) as f32,
681 height: (height * scale) as f32,
682 alignment: native_placeholder_alignment(alignment),
683 baseline: native_baseline(baseline.unwrap_or(TextBaseline::Alphabetic)),
684 baseline_offset: (baseline_offset * scale) as f32,
685 };
686 let style = self.current_style();
687 self.pieces.push(Piece::Placeholder(placeholder, style));
688 self.placeholder_scales.push(scale);
689 }
690
691 fn current_style(&self) -> valo::TextStyle {
693 self.style_stack
694 .last()
695 .expect("the base style is never popped")
696 .to_native()
697 }
698
699 pub fn build(self, fonts: &mut FontCollection) -> Paragraph {
705 let direction = self
706 .paragraph_style
707 .text_direction
708 .unwrap_or(TextDirection::Ltr);
709 let mut builder = valo::ParagraphBuilder::new(fonts);
710 builder.style(self.paragraph_style.to_native());
711 for piece in &self.pieces {
712 match piece {
713 Piece::Text(text, style) => builder.add_text(text, style),
714 Piece::Placeholder(placeholder, style) => {
715 builder.add_placeholder(*placeholder, style)
716 }
717 };
718 }
719 let inner = builder.build();
720 Paragraph {
721 code_units: CodeUnits::of(inner.text()),
722 inner,
723 direction,
724 layout_width: 0.0,
725 disposed: false,
726 }
727 }
728}
729
730#[derive(Clone, Debug)]
732struct CodeUnits {
733 byte_of: Vec<usize>,
735 len: usize,
737}
738
739impl CodeUnits {
740 fn of(text: &str) -> CodeUnits {
741 let mut byte_of = Vec::with_capacity(text.len() + 1);
742 for (byte, ch) in text.char_indices() {
743 byte_of.push(byte);
744 if ch.len_utf16() == 2 {
745 byte_of.push(byte);
747 }
748 }
749 byte_of.push(text.len());
750 let len = byte_of.len() - 1;
751 CodeUnits { byte_of, len }
752 }
753
754 fn byte(&self, code_unit: i32) -> usize {
755 let clamped = code_unit.clamp(0, self.len as i32) as usize;
756 self.byte_of[clamped]
757 }
758
759 fn code_unit(&self, byte: usize) -> i32 {
761 let first_at_or_after = self.byte_of.partition_point(|candidate| *candidate < byte);
762 match self.byte_of.get(first_at_or_after) {
763 Some(candidate) if *candidate == byte => first_at_or_after as i32,
764 _ => first_at_or_after.saturating_sub(1) as i32,
765 }
766 }
767}
768
769fn byte_of_code_unit(text: &str, code_unit: usize) -> usize {
771 CodeUnits::of(text).byte(code_unit as i32)
772}
773
774pub struct Paragraph {
783 inner: valo::Paragraph,
784 code_units: CodeUnits,
785 direction: TextDirection,
786 layout_width: f64,
788 disposed: bool,
789}
790
791impl Paragraph {
792 pub fn width(&self) -> f64 {
798 self.layout_width
799 }
800
801 pub fn height(&self) -> f64 {
805 f64::from(self.inner.height())
806 }
807
808 pub fn longest_line(&self) -> f64 {
811 f64::from(self.inner.longest_line())
812 }
813
814 pub fn min_intrinsic_width(&self) -> f64 {
817 f64::from(self.inner.min_intrinsic_width())
818 }
819
820 pub fn max_intrinsic_width(&self) -> f64 {
823 f64::from(self.inner.max_intrinsic_width())
824 }
825
826 pub fn alphabetic_baseline(&self) -> f64 {
829 self.inner
830 .lines()
831 .first()
832 .map_or(0.0, |line| f64::from(line.baseline))
833 }
834
835 pub fn ideographic_baseline(&self) -> f64 {
840 self.inner
841 .lines()
842 .first()
843 .map_or(0.0, |line| f64::from(line.baseline + line.descent))
844 }
845
846 pub fn did_exceed_max_lines(&self) -> bool {
850 self.inner.truncated()
851 }
852
853 pub fn layout(&mut self, constraints: ParagraphConstraints) {
857 debug_assert!(!self.disposed);
858 self.layout_width = constraints.width;
859 self.inner.layout(constraints.width as f32);
860 }
861
862 pub fn get_boxes_for_range(
867 &self,
868 start: i32,
869 end: i32,
870 box_height_style: BoxHeightStyle,
871 box_width_style: BoxWidthStyle,
872 ) -> Vec<TextBox> {
873 let _ = (box_height_style, box_width_style);
874 let range = self.code_units.byte(start)..self.code_units.byte(end);
875 self.inner
876 .rects_for_range(range)
877 .into_iter()
878 .map(|rect| text_box(rect, self.direction))
879 .collect()
880 }
881
882 pub fn get_boxes_for_placeholders(&self) -> Vec<TextBox> {
890 self.inner
891 .placeholder_boxes()
892 .into_iter()
893 .map(|placeholder| {
894 let direction = match placeholder.direction {
895 valo::TextDirection::Ltr => TextDirection::Ltr,
896 valo::TextDirection::Rtl => TextDirection::Rtl,
897 };
898 text_box(placeholder.rect, direction)
899 })
900 .collect()
901 }
902
903 pub fn get_position_for_offset(&self, offset: Offset) -> TextPosition {
905 let position = self.inner.glyph_position_at(offset.into());
906 TextPosition::with_affinity(
907 self.code_units.code_unit(position.offset),
908 if position.downstream {
909 TextAffinity::Downstream
910 } else {
911 TextAffinity::Upstream
912 },
913 )
914 }
915
916 pub fn get_closest_glyph_info_for_offset(&self, offset: Offset) -> Option<GlyphInfo> {
920 let position = self.inner.glyph_position_at(offset.into());
921 let anchor = if position.downstream {
922 position.offset
923 } else {
924 position.offset.saturating_sub(1)
925 };
926 self.glyph_info_at_byte(anchor)
927 }
928
929 pub fn get_glyph_info_at(&self, code_unit_offset: i32) -> Option<GlyphInfo> {
932 if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
933 return None;
934 }
935 self.glyph_info_at_byte(self.code_units.byte(code_unit_offset))
936 }
937
938 fn glyph_info_at_byte(&self, byte: usize) -> Option<GlyphInfo> {
941 for line in self.inner.lines() {
942 if !line.range.contains(&byte) {
943 continue;
944 }
945 let top = line.baseline - line.ascent;
946 let bottom = line.baseline + line.descent;
947 let cluster_starts = cluster_starts(line);
948 for run in &line.runs {
949 for glyph in &run.glyphs {
950 let cluster = cluster_range(&cluster_starts, glyph.cluster, line.range.end);
951 if !cluster.contains(&byte) {
952 continue;
953 }
954 let (left, right) = if glyph.advance >= 0.0 {
955 (glyph.x, glyph.x + glyph.advance)
956 } else {
957 (glyph.x + glyph.advance, glyph.x)
958 };
959 return Some(GlyphInfo {
960 grapheme_cluster_layout_bounds: Rect::from_ltrb(
961 f64::from(left),
962 f64::from(top),
963 f64::from(right),
964 f64::from(bottom),
965 ),
966 grapheme_cluster_code_unit_range: TextRange::new(
967 self.code_units.code_unit(cluster.start),
968 self.code_units.code_unit(cluster.end),
969 ),
970 writing_direction: if run.rtl {
971 TextDirection::Rtl
972 } else {
973 TextDirection::Ltr
974 },
975 });
976 }
977 }
978 }
979 None
980 }
981
982 pub fn get_word_boundary(&self, position: TextPosition) -> TextRange {
984 let range = self
985 .inner
986 .word_boundary(self.code_units.byte(position.offset));
987 self.text_range(range)
988 }
989
990 pub fn get_line_boundary(&self, position: TextPosition) -> TextRange {
994 let byte = self.code_units.byte(position.offset);
995 let Some(line) = self.line_containing(byte, position.affinity) else {
996 return TextRange::EMPTY;
997 };
998 let text = self.inner.text();
999 let mut end = line.range.end;
1000 if end > line.range.start && text.as_bytes().get(end - 1) == Some(&b'\n') {
1001 end -= 1;
1002 }
1003 self.text_range(line.range.start..end)
1004 }
1005
1006 fn line_containing(&self, byte: usize, affinity: TextAffinity) -> Option<&valo::Line> {
1007 let lines = self.inner.lines();
1008 let index = lines.iter().position(|line| {
1009 let starts_next_line = byte == line.range.start && affinity == TextAffinity::Upstream;
1010 (line.range.contains(&byte) && !starts_next_line) || byte == line.range.end
1011 });
1012 index.and_then(|index| lines.get(index)).or_else(|| {
1013 let last = lines.last()?;
1014 (byte >= last.range.end).then_some(last)
1015 })
1016 }
1017
1018 fn text_range(&self, range: Range<usize>) -> TextRange {
1019 TextRange::new(
1020 self.code_units.code_unit(range.start),
1021 self.code_units.code_unit(range.end),
1022 )
1023 }
1024
1025 pub fn compute_line_metrics(&self) -> Vec<LineMetrics> {
1028 let lines = self.inner.lines();
1029 let text = self.inner.text();
1030 lines
1031 .iter()
1032 .enumerate()
1033 .map(|(index, line)| {
1034 let is_last = index + 1 == lines.len();
1035 let ends_with_newline = line.range.end > line.range.start
1036 && text.as_bytes().get(line.range.end - 1) == Some(&b'\n');
1037 LineMetrics {
1038 hard_break: is_last || ends_with_newline,
1039 ascent: f64::from(line.ascent),
1040 descent: f64::from(line.descent),
1041 unscaled_ascent: f64::from(line.ascent),
1042 height: f64::from(line.ascent + line.descent),
1043 width: f64::from(line.width),
1044 left: f64::from(line.left),
1045 baseline: f64::from(line.baseline),
1046 line_number: index as i32,
1047 }
1048 })
1049 .collect()
1050 }
1051
1052 pub fn get_line_metrics_at(&self, line_number: i32) -> Option<LineMetrics> {
1055 if line_number < 0 {
1056 return None;
1057 }
1058 self.compute_line_metrics()
1059 .into_iter()
1060 .nth(line_number as usize)
1061 }
1062
1063 pub fn number_of_lines(&self) -> i32 {
1068 self.inner.lines().len() as i32
1069 }
1070
1071 pub fn get_line_number_at(&self, code_unit_offset: i32) -> Option<i32> {
1074 if code_unit_offset < 0 || code_unit_offset as usize >= self.code_units.len {
1075 return None;
1076 }
1077 let byte = self.code_units.byte(code_unit_offset);
1078 self.inner
1079 .lines()
1080 .iter()
1081 .position(|line| line.range.contains(&byte))
1082 .map(|index| index as i32)
1083 }
1084
1085 pub fn dispose(&mut self) {
1088 debug_assert!(!self.disposed);
1089 self.disposed = true;
1090 }
1091
1092 pub fn debug_disposed(&self) -> bool {
1094 self.disposed
1095 }
1096
1097 pub(crate) fn native(&self) -> &valo::Paragraph {
1099 &self.inner
1100 }
1101}
1102
1103fn text_box(rect: valo::Rect, direction: TextDirection) -> TextBox {
1104 TextBox::from_ltrbd(
1105 f64::from(rect.x),
1106 f64::from(rect.y),
1107 f64::from(rect.x + rect.width),
1108 f64::from(rect.y + rect.height),
1109 direction,
1110 )
1111}
1112
1113fn cluster_starts(line: &valo::Line) -> Vec<usize> {
1116 let mut starts: Vec<usize> = line
1117 .runs
1118 .iter()
1119 .flat_map(|run| run.glyphs.iter().map(|glyph| glyph.cluster))
1120 .collect();
1121 starts.sort_unstable();
1122 starts.dedup();
1123 starts
1124}
1125
1126fn cluster_range(cluster_starts: &[usize], start: usize, line_end: usize) -> Range<usize> {
1127 let next = cluster_starts
1128 .iter()
1129 .find(|candidate| **candidate > start)
1130 .copied()
1131 .unwrap_or(line_end);
1132 start..next.max(start + 1)
1133}
1134
1135impl fmt::Debug for Paragraph {
1136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1137 f.debug_struct("Paragraph")
1138 .field("text", &self.inner.text())
1139 .field("width", &self.width())
1140 .field("height", &self.height())
1141 .finish()
1142 }
1143}
1144
1145pub trait CanvasText {
1147 fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset);
1151}
1152
1153impl CanvasText for Canvas {
1154 fn draw_paragraph(&mut self, paragraph: &Paragraph, offset: Offset) {
1155 valo::DrawParagraphExt::draw_paragraph(self, paragraph.native(), offset);
1156 }
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161 use super::*;
1162
1163 #[test]
1164 fn code_units_map_surrogate_pairs_to_one_character() {
1165 let units = CodeUnits::of("a😀b");
1166 assert_eq!(units.len, 4);
1167 assert_eq!(units.byte(0), 0);
1168 assert_eq!(units.byte(1), 1);
1169 assert_eq!(
1170 units.byte(2),
1171 1,
1172 "the second half of the pair maps to the same byte"
1173 );
1174 assert_eq!(units.byte(3), 5);
1175 assert_eq!(units.byte(4), 6);
1176 assert_eq!(units.code_unit(5), 3);
1177 assert_eq!(units.code_unit(6), 4);
1178 }
1179
1180 #[test]
1184 fn a_spans_paints_reach_valo() {
1185 let outline = Paint {
1186 color: Color::from_argb(255, 0, 0, 0).into(),
1187 style: valo::PaintStyle::Stroke(valo::Stroke::new(2.0)),
1188 ..Paint::default()
1189 };
1190 let box_behind = Paint::from_color(Color::from_argb(160, 0, 0, 0).into());
1191 let style = TextStyle {
1192 foreground: Some(outline.clone()),
1193 background: Some(box_behind.clone()),
1194 ..TextStyle::default()
1195 };
1196 let native = style.to_native();
1197 assert_eq!(native.foreground, Some(outline.clone()));
1198 assert_eq!(native.background, Some(box_behind));
1199 assert_eq!(native.color, outline.color);
1200 }
1201
1202 #[test]
1203 fn text_range_slices_by_code_unit() {
1204 let range = TextRange::new(1, 3);
1205 assert_eq!(range.text_inside("a😀b"), "😀");
1206 assert_eq!(range.text_before("a😀b"), "a");
1207 assert_eq!(range.text_after("a😀b"), "b");
1208 assert!(TextRange::EMPTY.is_collapsed());
1209 assert!(!TextRange::EMPTY.is_valid());
1210 }
1211
1212 #[test]
1213 fn text_box_start_and_end_follow_direction() {
1214 let ltr = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Ltr);
1215 let rtl = TextBox::from_ltrbd(1.0, 2.0, 3.0, 4.0, TextDirection::Rtl);
1216 assert_eq!(ltr.start(), 1.0);
1217 assert_eq!(ltr.end(), 3.0);
1218 assert_eq!(rtl.start(), 3.0);
1219 assert_eq!(rtl.end(), 1.0);
1220 assert_eq!(ltr.to_rect(), Rect::from_ltrb(1.0, 2.0, 3.0, 4.0));
1221 }
1222
1223 fn text_styles(builder: &ParagraphBuilder) -> Vec<&valo::TextStyle> {
1224 builder
1225 .pieces
1226 .iter()
1227 .filter_map(|piece| match piece {
1228 Piece::Text(_, style) => Some(style),
1229 Piece::Placeholder(..) => None,
1230 })
1231 .collect()
1232 }
1233
1234 #[test]
1235 fn placeholders_are_counted_with_their_scales() {
1236 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1237 builder.add_text("a");
1238 builder.add_placeholder(10.0, 20.0, PlaceholderAlignment::Middle, 2.0, None, None);
1239 builder.add_placeholder(5.0, 5.0, PlaceholderAlignment::Bottom, 1.0, None, None);
1240 assert_eq!(builder.placeholder_count(), 2);
1241 assert_eq!(builder.placeholder_scales(), [2.0, 1.0]);
1242 }
1243
1244 #[test]
1245 fn pushed_styles_inherit_unset_fields() {
1246 let mut builder = ParagraphBuilder::new(ParagraphStyle {
1247 font_size: Some(20.0),
1248 ..ParagraphStyle::default()
1249 });
1250 builder.push_style(TextStyle {
1251 color: Some(Color::from_argb(255, 255, 0, 0)),
1252 ..TextStyle::default()
1253 });
1254 builder.add_text("x");
1255 builder.pop();
1256 builder.add_text("y");
1257 let styles = text_styles(&builder);
1258 assert_eq!(styles[0].size, 20.0, "the child keeps the paragraph's size");
1259 assert_eq!(styles[0].color, Color::from_argb(255, 255, 0, 0).into());
1260 assert_eq!(styles[1].size, 20.0);
1261 assert_eq!(
1262 styles[1].color,
1263 valo::Color::BLACK,
1264 "popping restores the base"
1265 );
1266 }
1267
1268 #[test]
1269 fn start_and_end_alignment_resolve_against_direction() {
1270 assert_eq!(
1271 native_align(Some(TextAlign::Start), Some(TextDirection::Rtl)),
1272 valo::TextAlign::Right
1273 );
1274 assert_eq!(
1275 native_align(Some(TextAlign::End), None),
1276 valo::TextAlign::Right,
1277 "no direction reads as LTR"
1278 );
1279 }
1280
1281 #[test]
1282 fn unspecified_font_family_stays_unset_on_the_native_style() {
1283 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1284 builder.add_text("x");
1285 assert!(
1286 text_styles(&builder)[0].families.is_empty(),
1287 "dart:ui leaves fontFamily unset; the collection's default manager supplies the face"
1288 );
1289 }
1290}
1291
1292#[cfg(test)]
1293mod layout_tests {
1294 use super::*;
1297 use crate::Platform;
1298
1299 fn fonts() -> FontCollection {
1300 let mut fonts = FontCollection::new();
1301 fonts.add_source(valo_system_fonts::SystemFonts::load());
1302 fonts
1303 }
1304
1305 fn paragraph(text: &str, style: ParagraphStyle, width: f64) -> Paragraph {
1306 let mut builder = ParagraphBuilder::new(style);
1307 builder.push_style(TextStyle {
1308 font_size: Some(20.0),
1309 ..TextStyle::default()
1310 });
1311 builder.add_text(text);
1312 let mut paragraph = builder.build(&mut fonts());
1313 paragraph.layout(ParagraphConstraints::new(width));
1314 paragraph
1315 }
1316
1317 #[test]
1318 fn a_laid_out_paragraph_has_a_size_and_one_line() {
1319 let paragraph = paragraph("Hello", ParagraphStyle::new(), f64::INFINITY);
1320 assert_eq!(
1321 paragraph.width(),
1322 f64::INFINITY,
1323 "the layout width, as in Skia"
1324 );
1325 assert!(paragraph.longest_line() > 0.0);
1326 assert!(paragraph.height() > 0.0);
1327 assert_eq!(paragraph.number_of_lines(), 1);
1328 assert!(paragraph.alphabetic_baseline() > 0.0);
1329 assert!(paragraph.alphabetic_baseline() < paragraph.height());
1330 assert!(!paragraph.did_exceed_max_lines());
1331 assert_eq!(paragraph.max_intrinsic_width(), paragraph.longest_line());
1332 }
1333
1334 #[test]
1335 fn a_narrow_width_wraps_onto_more_lines() {
1336 let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1337 let narrow = paragraph(
1338 "one two three four",
1339 ParagraphStyle::new(),
1340 wide.longest_line() / 2.0,
1341 );
1342 assert!(narrow.number_of_lines() > 1);
1343 assert!(narrow.height() > wide.height());
1344 let metrics = narrow.compute_line_metrics();
1345 assert_eq!(metrics.len() as i32, narrow.number_of_lines());
1346 assert!(
1347 metrics.last().unwrap().hard_break,
1348 "the last line is a hard break"
1349 );
1350 assert!(!metrics[0].hard_break, "a wrapped line is a soft break");
1351 assert_eq!(metrics[1].line_number, 1);
1352 }
1353
1354 #[test]
1355 fn max_lines_with_an_ellipsis_truncates() {
1356 let wide = paragraph("one two three four", ParagraphStyle::new(), f64::INFINITY);
1357 let style = ParagraphStyle {
1358 max_lines: Some(1),
1359 ellipsis: Some("\u{2026}".to_owned()),
1360 ..ParagraphStyle::default()
1361 };
1362 let clipped = paragraph("one two three four", style, wide.longest_line() / 2.0);
1363 assert_eq!(clipped.number_of_lines(), 1);
1364 assert!(clipped.did_exceed_max_lines());
1365 }
1366
1367 #[test]
1368 fn positions_boxes_and_boundaries_use_code_units() {
1369 let paragraph = paragraph("ab cd", ParagraphStyle::new(), f64::INFINITY);
1370 let start = paragraph.get_position_for_offset(Offset::new(0.0, 1.0));
1371 assert_eq!(start.offset, 0);
1372 let end =
1373 paragraph.get_position_for_offset(Offset::new(paragraph.longest_line() + 50.0, 1.0));
1374 assert_eq!(end.offset, 5);
1375
1376 let boxes =
1377 paragraph.get_boxes_for_range(0, 2, BoxHeightStyle::Tight, BoxWidthStyle::Tight);
1378 assert_eq!(boxes.len(), 1);
1379 assert!(boxes[0].right > boxes[0].left);
1380 assert_eq!(boxes[0].direction, TextDirection::Ltr);
1381
1382 assert_eq!(
1383 paragraph.get_word_boundary(TextPosition::new(4)),
1384 TextRange::new(3, 5)
1385 );
1386 assert_eq!(
1387 paragraph.get_line_boundary(TextPosition::new(1)),
1388 TextRange::new(0, 5)
1389 );
1390 assert_eq!(paragraph.get_line_number_at(4), Some(0));
1391 assert_eq!(paragraph.get_line_number_at(5), None);
1392
1393 let glyph = paragraph.get_glyph_info_at(3).expect("the c glyph");
1394 assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(3, 4));
1395 assert!(glyph.grapheme_cluster_layout_bounds.width() > 0.0);
1396 assert!(paragraph.get_glyph_info_at(5).is_none());
1397 }
1398
1399 #[test]
1400 fn a_surrogate_pair_is_one_glyph_of_two_code_units() {
1401 let paragraph = paragraph("a😀b", ParagraphStyle::new(), f64::INFINITY);
1402 let glyph = paragraph.get_glyph_info_at(1).expect("the emoji");
1403 assert_eq!(glyph.grapheme_cluster_code_unit_range, TextRange::new(1, 3));
1404 assert_eq!(
1405 paragraph
1406 .get_glyph_info_at(3)
1407 .unwrap()
1408 .grapheme_cluster_code_unit_range,
1409 TextRange::new(3, 4)
1410 );
1411 }
1412
1413 #[test]
1416 fn the_closest_glyph_is_the_one_under_the_point() {
1417 let paragraph = paragraph("abc", ParagraphStyle::new(), f64::INFINITY);
1418 let b = paragraph.get_glyph_info_at(1).expect("the b glyph");
1419 let bounds = b.grapheme_cluster_layout_bounds;
1420 let y = bounds.center().dy();
1421 for x in [bounds.left + 0.5, bounds.right - 0.5] {
1422 let closest = paragraph
1423 .get_closest_glyph_info_for_offset(Offset::new(x, y))
1424 .expect("a glyph");
1425 assert_eq!(
1426 closest.grapheme_cluster_code_unit_range,
1427 TextRange::new(1, 2)
1428 );
1429 }
1430 let past_the_end = paragraph
1431 .get_closest_glyph_info_for_offset(Offset::new(1000.0, y))
1432 .expect("a glyph");
1433 assert_eq!(
1434 past_the_end.grapheme_cluster_code_unit_range,
1435 TextRange::new(2, 3)
1436 );
1437 }
1438
1439 #[test]
1440 fn placeholder_boxes_come_in_the_order_added_at_their_scaled_size() {
1441 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1442 builder.push_style(TextStyle {
1443 font_size: Some(20.0),
1444 ..TextStyle::default()
1445 });
1446 builder.add_text("ab");
1447 builder.add_placeholder(
1448 30.0,
1449 50.0,
1450 PlaceholderAlignment::Baseline,
1451 1.0,
1452 Some(40.0),
1453 Some(TextBaseline::Alphabetic),
1454 );
1455 builder.add_text("cd");
1456 builder.add_placeholder(10.0, 10.0, PlaceholderAlignment::Bottom, 2.0, None, None);
1457 let mut paragraph = builder.build(&mut fonts());
1458 paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1459
1460 let boxes = paragraph.get_boxes_for_placeholders();
1461 assert_eq!(boxes.len(), 2);
1462 let baseline = paragraph.alphabetic_baseline();
1463 let (first, second) = (boxes[0], boxes[1]);
1464 assert!((first.right - first.left - 30.0).abs() < 1e-3);
1465 assert!(
1466 (first.top - (baseline - 40.0)).abs() < 1e-3,
1467 "40 above the baseline"
1468 );
1469 assert!(
1470 (first.bottom - (baseline + 10.0)).abs() < 1e-3,
1471 "10 below it"
1472 );
1473 assert!(
1474 (second.right - second.left - 20.0).abs() < 1e-3,
1475 "scaled by 2"
1476 );
1477 assert!((second.bottom - second.top - 20.0).abs() < 1e-3);
1478 assert!(second.left > first.right, "after the text between them");
1479 assert_eq!(
1480 paragraph.get_line_boundary(TextPosition::new(0)),
1481 TextRange::new(0, 6),
1482 "each placeholder is one code unit"
1483 );
1484 }
1485
1486 #[test]
1487 fn drawing_records_the_paragraph() {
1488 let paragraph = paragraph("Hi", ParagraphStyle::new(), f64::INFINITY);
1489 let mut canvas = Canvas::new();
1490 canvas.draw_paragraph(¶graph, Offset::new(10.0, 10.0));
1491 let picture = canvas.build();
1492 assert!(!picture.ops().is_empty());
1493 }
1494
1495 #[test]
1496 fn unspecified_family_at_black_weight_covers_latin() {
1497 let mut fonts = FontCollection::new();
1498 crate::set_default_font_manager(
1499 &mut fonts,
1500 Box::new(crate::SystemFontSource::platform()),
1501 &crate::InertPlatform.default_font_families(),
1502 );
1503 let mut builder = ParagraphBuilder::new(ParagraphStyle::new());
1504 builder.push_style(TextStyle {
1505 font_size: Some(10.2),
1506 font_weight: Some(FontWeight::W900),
1507 ..TextStyle::default()
1508 });
1509 builder.add_text("DEBUG");
1510 let mut paragraph = builder.build(&mut fonts);
1511 paragraph.layout(ParagraphConstraints::new(f64::INFINITY));
1512 assert!(paragraph.longest_line() > 0.0);
1513 assert!(
1514 fonts.take_unanswered().is_empty(),
1515 "the engine default family covers Latin at weight 900"
1516 );
1517 for line in paragraph.inner.lines() {
1518 for run in &line.runs {
1519 assert!(
1520 run.glyphs.iter().all(|glyph| glyph.id != 0),
1521 "no .notdef tofu"
1522 );
1523 }
1524 }
1525 }
1526}