1use crate::{
2 TextBlobId,
3 geometry::{Point, Px, Rect},
4 ids::FontId,
5};
6use serde::{Deserialize, Serialize};
7use smol_str::SmolStr;
8use std::sync::Arc;
9
10use crate::scene::Color;
11
12#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
23pub struct TextFontFamilyConfig {
24 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 pub ui_sans: Vec<String>,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub ui_serif: Vec<String>,
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub ui_mono: Vec<String>,
30 #[serde(
39 default,
40 skip_serializing_if = "TextCommonFallbackInjection::is_platform_default"
41 )]
42 pub common_fallback_injection: TextCommonFallbackInjection,
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub common_fallback: Vec<String>,
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum TextCommonFallbackInjection {
54 #[default]
55 PlatformDefault,
56 None,
57 CommonFallback,
58}
59
60impl TextCommonFallbackInjection {
61 fn is_platform_default(v: &TextCommonFallbackInjection) -> bool {
62 *v == TextCommonFallbackInjection::PlatformDefault
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(transparent)]
68pub struct FontWeight(pub u16);
69
70impl FontWeight {
71 pub const THIN: Self = Self(100);
72 pub const EXTRA_LIGHT: Self = Self(200);
73 pub const LIGHT: Self = Self(300);
74 pub const NORMAL: Self = Self(400);
75 pub const MEDIUM: Self = Self(500);
76 pub const SEMIBOLD: Self = Self(600);
77 pub const BOLD: Self = Self(700);
78 pub const EXTRA_BOLD: Self = Self(800);
79 pub const BLACK: Self = Self(900);
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub enum TextWrap {
84 None,
85 Word,
86 Balance,
94 WordBreak,
100 Grapheme,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
108pub enum TextOverflow {
109 #[default]
110 Clip,
111 Ellipsis,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
115pub enum TextAlign {
116 #[default]
117 Start,
118 Center,
119 End,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum TextVerticalPlacement {
125 #[default]
129 CenterMetricsBox,
130 BoundsAsLineBox,
142}
143
144impl TextVerticalPlacement {
145 fn is_center_metrics_box(v: &TextVerticalPlacement) -> bool {
146 *v == TextVerticalPlacement::CenterMetricsBox
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum TextLineHeightPolicy {
153 #[default]
158 ExpandToFit,
159 FixedFromStyle,
165}
166
167impl TextLineHeightPolicy {
168 fn is_expand_to_fit(v: &TextLineHeightPolicy) -> bool {
169 *v == TextLineHeightPolicy::ExpandToFit
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub enum TextLeadingDistribution {
176 #[default]
178 Even,
179 Proportional,
181}
182
183impl TextLeadingDistribution {
184 fn is_even(v: &TextLeadingDistribution) -> bool {
185 *v == TextLeadingDistribution::Even
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
194pub struct TextStrutStyle {
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub font: Option<FontId>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub size: Option<Px>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub line_height: Option<Px>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub line_height_em: Option<f32>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub leading_distribution: Option<TextLeadingDistribution>,
210 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
215 pub force: bool,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq)]
219pub struct TextConstraints {
220 pub max_width: Option<Px>,
221 pub wrap: TextWrap,
222 pub overflow: TextOverflow,
223 pub align: TextAlign,
224 pub scale_factor: f32,
230}
231
232impl Default for TextConstraints {
233 fn default() -> Self {
234 Self {
235 max_width: None,
236 wrap: TextWrap::Word,
237 overflow: TextOverflow::Clip,
238 align: TextAlign::Start,
239 scale_factor: 1.0,
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct TextStyle {
246 pub font: FontId,
247 pub size: Px,
248 pub weight: FontWeight,
249 pub slant: TextSlant,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub line_height: Option<Px>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub line_height_em: Option<f32>,
256 #[serde(
258 default,
259 skip_serializing_if = "TextLineHeightPolicy::is_expand_to_fit"
260 )]
261 pub line_height_policy: TextLineHeightPolicy,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub letter_spacing_em: Option<f32>,
265 #[serde(default, skip_serializing_if = "Vec::is_empty")]
270 pub features: Vec<TextFontFeatureSetting>,
271 #[serde(default, skip_serializing_if = "Vec::is_empty")]
276 pub axes: Vec<TextFontAxisSetting>,
277 #[serde(
282 default,
283 skip_serializing_if = "TextVerticalPlacement::is_center_metrics_box"
284 )]
285 pub vertical_placement: TextVerticalPlacement,
286 #[serde(default, skip_serializing_if = "TextLeadingDistribution::is_even")]
289 pub leading_distribution: TextLeadingDistribution,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub strut_style: Option<TextStrutStyle>,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
297#[serde(rename_all = "snake_case")]
298pub enum TextSlant {
299 #[default]
300 Normal,
301 Italic,
302 Oblique,
303}
304
305impl Default for TextStyle {
306 fn default() -> Self {
307 Self {
308 font: FontId::default(),
309 size: Px(13.0),
310 weight: FontWeight::NORMAL,
311 slant: TextSlant::Normal,
312 line_height: None,
313 line_height_em: None,
314 line_height_policy: TextLineHeightPolicy::ExpandToFit,
315 letter_spacing_em: None,
316 features: Vec::new(),
317 axes: Vec::new(),
318 vertical_placement: TextVerticalPlacement::CenterMetricsBox,
319 leading_distribution: TextLeadingDistribution::Even,
320 strut_style: None,
321 }
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
330pub struct TextStyleRefinement {
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub font: Option<FontId>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub size: Option<Px>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub weight: Option<FontWeight>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub slant: Option<TextSlant>,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub line_height: Option<Px>,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub line_height_em: Option<f32>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub line_height_policy: Option<TextLineHeightPolicy>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub letter_spacing_em: Option<f32>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub vertical_placement: Option<TextVerticalPlacement>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub leading_distribution: Option<TextLeadingDistribution>,
351}
352
353impl TextStyleRefinement {
354 pub fn is_empty(&self) -> bool {
355 self.font.is_none()
356 && self.size.is_none()
357 && self.weight.is_none()
358 && self.slant.is_none()
359 && self.line_height.is_none()
360 && self.line_height_em.is_none()
361 && self.line_height_policy.is_none()
362 && self.letter_spacing_em.is_none()
363 && self.vertical_placement.is_none()
364 && self.leading_distribution.is_none()
365 }
366
367 pub fn merge(&mut self, other: &Self) {
368 if let Some(font) = other.font.clone() {
369 self.font = Some(font);
370 }
371 if let Some(size) = other.size {
372 self.size = Some(size);
373 }
374 if let Some(weight) = other.weight {
375 self.weight = Some(weight);
376 }
377 if let Some(slant) = other.slant {
378 self.slant = Some(slant);
379 }
380 if let Some(line_height) = other.line_height {
381 self.line_height = Some(line_height);
382 self.line_height_em = None;
383 } else if let Some(line_height_em) = other.line_height_em {
384 self.line_height_em = Some(line_height_em);
385 self.line_height = None;
386 }
387 if let Some(line_height_policy) = other.line_height_policy {
388 self.line_height_policy = Some(line_height_policy);
389 }
390 if let Some(letter_spacing_em) = other.letter_spacing_em {
391 self.letter_spacing_em = Some(letter_spacing_em);
392 }
393 if let Some(vertical_placement) = other.vertical_placement {
394 self.vertical_placement = Some(vertical_placement);
395 }
396 if let Some(leading_distribution) = other.leading_distribution {
397 self.leading_distribution = Some(leading_distribution);
398 }
399 }
400
401 pub fn merged(&self, other: &Self) -> Self {
402 let mut merged = self.clone();
403 merged.merge(other);
404 merged
405 }
406}
407
408impl TextStyle {
409 pub fn refine(&mut self, refinement: &TextStyleRefinement) {
410 if let Some(font) = refinement.font.clone() {
411 self.font = font;
412 }
413 if let Some(size) = refinement.size {
414 self.size = size;
415 }
416 if let Some(weight) = refinement.weight {
417 self.weight = weight;
418 }
419 if let Some(slant) = refinement.slant {
420 self.slant = slant;
421 }
422 if let Some(line_height) = refinement.line_height {
423 self.line_height = Some(line_height);
424 self.line_height_em = None;
425 } else if let Some(line_height_em) = refinement.line_height_em {
426 self.line_height_em = Some(line_height_em);
427 self.line_height = None;
428 }
429 if let Some(line_height_policy) = refinement.line_height_policy {
430 self.line_height_policy = line_height_policy;
431 }
432 if let Some(letter_spacing_em) = refinement.letter_spacing_em {
433 self.letter_spacing_em = Some(letter_spacing_em);
434 }
435 if let Some(vertical_placement) = refinement.vertical_placement {
436 self.vertical_placement = vertical_placement;
437 }
438 if let Some(leading_distribution) = refinement.leading_distribution {
439 self.leading_distribution = leading_distribution;
440 }
441 }
442
443 pub fn refined(mut self, refinement: &TextStyleRefinement) -> Self {
444 self.refine(refinement);
445 self
446 }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq)]
450pub struct TextMetrics {
451 pub size: crate::Size,
452 pub baseline: Px,
453}
454
455#[derive(Debug, Clone, Copy, PartialEq)]
456pub struct TextLineMetrics {
457 pub ascent: Px,
458 pub descent: Px,
459 pub line_height: Px,
460}
461
462#[derive(Debug, Clone, Copy, PartialEq)]
463pub struct TextInkMetrics {
464 pub ascent: Px,
465 pub descent: Px,
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469pub enum CaretAffinity {
470 Upstream,
471 Downstream,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq)]
475pub struct HitTestResult {
476 pub index: usize,
477 pub affinity: CaretAffinity,
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
481#[serde(rename_all = "snake_case")]
482pub enum DecorationLineStyle {
483 #[default]
484 Solid,
485}
486
487#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
488pub struct UnderlineStyle {
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub color: Option<Color>,
491 #[serde(default)]
492 pub style: DecorationLineStyle,
493}
494
495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496pub struct StrikethroughStyle {
497 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub color: Option<Color>,
499 #[serde(default)]
500 pub style: DecorationLineStyle,
501}
502
503#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
504pub struct TextShapingStyle {
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub font: Option<FontId>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub weight: Option<FontWeight>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub slant: Option<TextSlant>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub letter_spacing_em: Option<f32>,
513 #[serde(default, skip_serializing_if = "Vec::is_empty")]
519 pub features: Vec<TextFontFeatureSetting>,
520 #[serde(default, skip_serializing_if = "Vec::is_empty")]
526 pub axes: Vec<TextFontAxisSetting>,
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
531pub struct TextFontFeatureSetting {
532 pub tag: SmolStr,
534 pub value: u32,
536}
537
538#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
540pub struct TextFontAxisSetting {
541 pub tag: SmolStr,
542 pub value: f32,
543}
544
545#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
546pub struct TextPaintStyle {
547 #[serde(default, skip_serializing_if = "Option::is_none")]
548 pub fg: Option<Color>,
549 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub bg: Option<Color>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub underline: Option<UnderlineStyle>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub strikethrough: Option<StrikethroughStyle>,
555}
556
557#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
558pub struct TextSpan {
559 pub len: usize,
561 #[serde(default)]
562 pub shaping: TextShapingStyle,
563 #[serde(default)]
564 pub paint: TextPaintStyle,
565}
566
567impl TextSpan {
568 pub fn new(len: usize) -> Self {
569 Self {
570 len,
571 shaping: TextShapingStyle::default(),
572 paint: TextPaintStyle::default(),
573 }
574 }
575}
576
577impl TextShapingStyle {
578 pub fn with_font(mut self, font: FontId) -> Self {
579 self.font = Some(font);
580 self
581 }
582
583 pub fn with_weight(mut self, weight: FontWeight) -> Self {
584 self.weight = Some(weight);
585 self
586 }
587
588 pub fn with_slant(mut self, slant: TextSlant) -> Self {
589 self.slant = Some(slant);
590 self
591 }
592
593 pub fn with_letter_spacing_em(mut self, letter_spacing_em: f32) -> Self {
594 self.letter_spacing_em = Some(letter_spacing_em);
595 self
596 }
597
598 pub fn with_axis(mut self, tag: impl Into<String>, value: f32) -> Self {
599 self.axes.push(TextFontAxisSetting {
600 tag: tag.into().into(),
601 value,
602 });
603 self
604 }
605
606 pub fn with_feature(mut self, tag: impl Into<String>, value: u32) -> Self {
607 self.features.push(TextFontFeatureSetting {
608 tag: tag.into().into(),
609 value,
610 });
611 self
612 }
613}
614
615impl TextPaintStyle {
616 pub fn with_fg(mut self, fg: Color) -> Self {
617 self.fg = Some(fg);
618 self
619 }
620
621 pub fn with_bg(mut self, bg: Color) -> Self {
622 self.bg = Some(bg);
623 self
624 }
625
626 pub fn with_underline(mut self, underline: UnderlineStyle) -> Self {
627 self.underline = Some(underline);
628 self
629 }
630
631 pub fn with_strikethrough(mut self, strikethrough: StrikethroughStyle) -> Self {
632 self.strikethrough = Some(strikethrough);
633 self
634 }
635}
636
637#[derive(Debug, Clone, PartialEq)]
638pub struct AttributedText {
639 pub text: Arc<str>,
640 pub spans: Arc<[TextSpan]>,
641}
642
643fn spans_are_valid(text: &str, spans: &[TextSpan]) -> bool {
644 let mut offset = 0usize;
645 for span in spans {
646 let end = offset.saturating_add(span.len);
647 if end > text.len() {
648 return false;
649 }
650 if !text.is_char_boundary(offset) || !text.is_char_boundary(end) {
651 return false;
652 }
653 offset = end;
654 }
655 offset == text.len()
656}
657
658impl AttributedText {
659 pub fn new(text: impl Into<Arc<str>>, spans: impl Into<Arc<[TextSpan]>>) -> Self {
660 let text: Arc<str> = text.into();
661 let spans: Arc<[TextSpan]> = spans.into();
662 debug_assert!(spans_are_valid(text.as_ref(), spans.as_ref()));
663 Self { text, spans }
664 }
665
666 pub fn shaping_eq(&self, other: &Self) -> bool {
671 if self.text != other.text {
672 return false;
673 }
674 if self.spans.len() != other.spans.len() {
675 return false;
676 }
677 self.spans
678 .iter()
679 .zip(other.spans.iter())
680 .all(|(a, b)| a.len == b.len && a.shaping == b.shaping)
681 }
682
683 pub fn is_valid(&self) -> bool {
684 spans_are_valid(self.text.as_ref(), self.spans.as_ref())
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn attributed_text_shaping_eq_ignores_paint() {
694 let text: Arc<str> = Arc::<str>::from("hello");
695 let base = TextSpan {
696 len: text.len(),
697 shaping: Default::default(),
698 paint: Default::default(),
699 };
700
701 let mut spans_a = vec![base.clone()];
702 spans_a[0].paint.fg = Some(Color {
703 r: 1.0,
704 g: 0.0,
705 b: 0.0,
706 a: 1.0,
707 });
708 let mut spans_b = vec![base];
709 spans_b[0].paint.fg = Some(Color {
710 r: 0.0,
711 g: 1.0,
712 b: 0.0,
713 a: 1.0,
714 });
715
716 let a = AttributedText::new(Arc::clone(&text), Arc::<[TextSpan]>::from(spans_a));
717 let b = AttributedText::new(Arc::clone(&text), Arc::<[TextSpan]>::from(spans_b));
718 assert_ne!(a, b, "full equality should include paint");
719 assert!(
720 a.shaping_eq(&b),
721 "shaping_eq should ignore paint-only changes"
722 );
723 }
724
725 #[test]
726 fn attributed_text_shaping_eq_detects_shaping_changes() {
727 let text: Arc<str> = Arc::<str>::from("hello");
728 let spans_a = vec![TextSpan {
729 len: text.len(),
730 shaping: Default::default(),
731 paint: Default::default(),
732 }];
733 let mut spans_b = spans_a.clone();
734 spans_b[0].shaping.weight = Some(FontWeight(700));
735
736 let a = AttributedText::new(Arc::clone(&text), Arc::<[TextSpan]>::from(spans_a));
737 let b = AttributedText::new(Arc::clone(&text), Arc::<[TextSpan]>::from(spans_b));
738 assert!(
739 !a.shaping_eq(&b),
740 "shaping_eq must treat shaping changes as unequal"
741 );
742 }
743}
744
745#[derive(Debug, Clone, Copy, PartialEq)]
746pub enum TextInputRef<'a> {
747 Plain {
748 text: &'a str,
749 style: &'a TextStyle,
750 },
751 Attributed {
752 text: &'a str,
753 base: &'a TextStyle,
754 spans: &'a [TextSpan],
755 },
756}
757
758impl<'a> TextInputRef<'a> {
759 pub fn plain(text: &'a str, style: &'a TextStyle) -> Self {
760 Self::Plain { text, style }
761 }
762
763 pub fn attributed(text: &'a str, base: &'a TextStyle, spans: &'a [TextSpan]) -> Self {
764 debug_assert!(spans_are_valid(text, spans));
765 Self::Attributed { text, base, spans }
766 }
767}
768
769#[derive(Debug, Clone, PartialEq)]
770#[non_exhaustive]
771pub enum TextInput {
772 Plain {
773 text: Arc<str>,
774 style: TextStyle,
775 },
776 Attributed {
777 text: Arc<str>,
778 base: TextStyle,
779 spans: Arc<[TextSpan]>,
780 },
781}
782
783impl TextInput {
784 pub fn plain(text: impl Into<Arc<str>>, style: TextStyle) -> Self {
785 Self::Plain {
786 text: text.into(),
787 style,
788 }
789 }
790
791 pub fn attributed(
792 text: impl Into<Arc<str>>,
793 base: TextStyle,
794 spans: impl Into<Arc<[TextSpan]>>,
795 ) -> Self {
796 Self::Attributed {
797 text: text.into(),
798 base,
799 spans: spans.into(),
800 }
801 }
802
803 pub fn text(&self) -> &str {
804 match self {
805 Self::Plain { text, .. } => text.as_ref(),
806 Self::Attributed { text, .. } => text.as_ref(),
807 }
808 }
809}
810
811pub trait TextService {
812 fn prepare(
813 &mut self,
814 input: &TextInput,
815 constraints: TextConstraints,
816 ) -> (TextBlobId, TextMetrics);
817
818 fn prepare_str(
819 &mut self,
820 text: &str,
821 style: &TextStyle,
822 constraints: TextConstraints,
823 ) -> (TextBlobId, TextMetrics) {
824 let input = TextInput::plain(Arc::<str>::from(text), style.clone());
825 self.prepare(&input, constraints)
826 }
827
828 fn prepare_rich(
829 &mut self,
830 rich: &AttributedText,
831 base_style: &TextStyle,
832 constraints: TextConstraints,
833 ) -> (TextBlobId, TextMetrics) {
834 let input =
835 TextInput::attributed(rich.text.clone(), base_style.clone(), rich.spans.clone());
836 self.prepare(&input, constraints)
837 }
838
839 fn measure(&mut self, input: &TextInput, constraints: TextConstraints) -> TextMetrics {
840 let (blob, metrics) = self.prepare(input, constraints);
841 self.release(blob);
842 metrics
843 }
844
845 fn measure_str(
846 &mut self,
847 text: &str,
848 style: &TextStyle,
849 constraints: TextConstraints,
850 ) -> TextMetrics {
851 let input = TextInput::plain(Arc::<str>::from(text), style.clone());
852 self.measure(&input, constraints)
853 }
854
855 fn measure_rich(
856 &mut self,
857 rich: &AttributedText,
858 base_style: &TextStyle,
859 constraints: TextConstraints,
860 ) -> TextMetrics {
861 let (blob, metrics) = self.prepare_rich(rich, base_style, constraints);
862 self.release(blob);
863 metrics
864 }
865
866 fn caret_x(&mut self, _blob: TextBlobId, _index: usize) -> Px {
874 Px(0.0)
875 }
876
877 fn hit_test_x(&mut self, _blob: TextBlobId, _x: Px) -> usize {
881 0
882 }
883
884 fn selection_rects(&mut self, _blob: TextBlobId, _range: (usize, usize), _out: &mut Vec<Rect>) {
892 }
893
894 fn first_line_metrics(&mut self, _blob: TextBlobId) -> Option<TextLineMetrics> {
900 None
901 }
902
903 fn first_line_ink_metrics(&mut self, _blob: TextBlobId) -> Option<TextInkMetrics> {
910 None
911 }
912
913 fn last_line_metrics(&mut self, _blob: TextBlobId) -> Option<TextLineMetrics> {
919 None
920 }
921
922 fn last_line_ink_metrics(&mut self, _blob: TextBlobId) -> Option<TextInkMetrics> {
927 None
928 }
929
930 fn selection_rects_clipped(
937 &mut self,
938 blob: TextBlobId,
939 range: (usize, usize),
940 clip: Rect,
941 out: &mut Vec<Rect>,
942 ) {
943 self.selection_rects(blob, range, out);
944 clip_rects_in_place(clip, out);
945 }
946
947 fn caret_stops(&mut self, _blob: TextBlobId, _out: &mut Vec<(usize, Px)>) {}
952
953 fn caret_rect(&mut self, _blob: TextBlobId, _index: usize, _affinity: CaretAffinity) -> Rect {
962 Rect::default()
963 }
964
965 fn hit_test_point(&mut self, _blob: TextBlobId, _point: Point) -> HitTestResult {
969 HitTestResult {
970 index: 0,
971 affinity: CaretAffinity::Downstream,
972 }
973 }
974
975 fn release(&mut self, blob: TextBlobId);
976}
977
978fn clip_rects_in_place(clip: Rect, out: &mut Vec<Rect>) {
979 let clip_x0 = clip.origin.x.0;
980 let clip_y0 = clip.origin.y.0;
981 let clip_x1 = clip_x0 + clip.size.width.0;
982 let clip_y1 = clip_y0 + clip.size.height.0;
983
984 if clip_x1 <= clip_x0 || clip_y1 <= clip_y0 {
985 out.clear();
986 return;
987 }
988
989 out.retain_mut(|r| {
990 let x0 = r.origin.x.0;
991 let y0 = r.origin.y.0;
992 let x1 = x0 + r.size.width.0;
993 let y1 = y0 + r.size.height.0;
994
995 let ix0 = x0.max(clip_x0);
996 let iy0 = y0.max(clip_y0);
997 let ix1 = x1.min(clip_x1);
998 let iy1 = y1.min(clip_y1);
999
1000 if ix1 <= ix0 || iy1 <= iy0 {
1001 return false;
1002 }
1003
1004 r.origin.x = Px(ix0);
1005 r.origin.y = Px(iy0);
1006 r.size.width = Px(ix1 - ix0);
1007 r.size.height = Px(iy1 - iy0);
1008 true
1009 });
1010}