Skip to main content

cranpose_ui/text/
style.rs

1use super::decoration::{Shadow, TextDecoration};
2use super::font::{FontFamily, FontStyle, FontSynthesis, FontWeight};
3use super::paragraph::{Hyphens, LineBreak, TextAlign, TextDirection, TextIndent};
4use super::unit::TextUnit;
5use crate::modifier::{Brush, Color};
6use std::collections::hash_map::DefaultHasher;
7use std::hash::{Hash, Hasher};
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct BaselineShift(pub f32);
11
12impl BaselineShift {
13    pub const SUPERSCRIPT: Self = Self(0.5);
14    pub const SUBSCRIPT: Self = Self(-0.5);
15    pub const NONE: Self = Self(0.0);
16    pub const UNSPECIFIED: Self = Self(f32::NAN);
17
18    pub fn is_specified(self) -> bool {
19        !self.0.is_nan()
20    }
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct TextGeometricTransform {
25    pub scale_x: f32,
26    pub skew_x: f32,
27}
28
29impl Default for TextGeometricTransform {
30    fn default() -> Self {
31        Self {
32            scale_x: 1.0,
33            skew_x: 0.0,
34        }
35    }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
39pub struct LocaleList {
40    locales: Vec<String>,
41}
42
43impl LocaleList {
44    pub fn new(locales: Vec<String>) -> Self {
45        Self { locales }
46    }
47
48    pub fn from_language_tags(tags: &str) -> Self {
49        let locales = tags
50            .split(',')
51            .map(str::trim)
52            .filter(|tag| !tag.is_empty())
53            .map(ToString::to_string)
54            .collect();
55        Self { locales }
56    }
57
58    pub fn locales(&self) -> &[String] {
59        &self.locales
60    }
61
62    pub fn is_empty(&self) -> bool {
63        self.locales.is_empty()
64    }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
68pub enum LineHeightAlignment {
69    Top,
70    Center,
71    #[default]
72    Proportional,
73    Bottom,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
77pub enum LineHeightTrim {
78    FirstLineTop,
79    LastLineBottom,
80    #[default]
81    Both,
82    None,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
86pub enum LineHeightMode {
87    #[default]
88    Fixed,
89    Minimum,
90    Tight,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94pub struct LineHeightStyle {
95    pub alignment: LineHeightAlignment,
96    pub trim: LineHeightTrim,
97    pub mode: LineHeightMode,
98}
99
100impl Default for LineHeightStyle {
101    fn default() -> Self {
102        Self {
103            alignment: LineHeightAlignment::Proportional,
104            trim: LineHeightTrim::Both,
105            mode: LineHeightMode::Fixed,
106        }
107    }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
111pub enum TextMotion {
112    #[default]
113    Static,
114    Animated,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
118pub struct PlatformSpanStyle;
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
121pub struct PlatformParagraphStyle {
122    pub include_font_padding: Option<bool>,
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
126pub struct PlatformTextStyle {
127    pub span_style: Option<PlatformSpanStyle>,
128    pub paragraph_style: Option<PlatformParagraphStyle>,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq)]
132pub enum TextDrawStyle {
133    Fill,
134    Stroke { width: f32 },
135}
136
137impl Default for TextDrawStyle {
138    fn default() -> Self {
139        Self::Fill
140    }
141}
142
143#[derive(Clone, Debug, PartialEq)]
144pub struct SpanStyle {
145    pub color: Option<Color>,
146    pub brush: Option<Brush>,
147    pub alpha: Option<f32>,
148    pub font_size: TextUnit,
149    pub font_weight: Option<FontWeight>,
150    pub font_style: Option<FontStyle>,
151    pub font_synthesis: Option<FontSynthesis>,
152    pub font_family: Option<FontFamily>,
153    pub font_feature_settings: Option<String>,
154    pub letter_spacing: TextUnit,
155    pub baseline_shift: Option<BaselineShift>,
156    pub text_geometric_transform: Option<TextGeometricTransform>,
157    pub locale_list: Option<LocaleList>,
158    pub background: Option<Color>,
159    pub text_decoration: Option<TextDecoration>,
160    pub shadow: Option<Shadow>,
161    pub platform_style: Option<PlatformSpanStyle>,
162    pub draw_style: Option<TextDrawStyle>,
163}
164
165impl Default for SpanStyle {
166    fn default() -> Self {
167        Self {
168            color: None,
169            brush: None,
170            alpha: None,
171            font_size: TextUnit::Unspecified,
172            font_weight: None,
173            font_style: None,
174            font_synthesis: None,
175            font_family: None,
176            font_feature_settings: None,
177            letter_spacing: TextUnit::Unspecified,
178            baseline_shift: None,
179            text_geometric_transform: None,
180            locale_list: None,
181            background: None,
182            text_decoration: None,
183            shadow: None,
184            platform_style: None,
185            draw_style: None,
186        }
187    }
188}
189
190impl SpanStyle {
191    pub fn merge(&self, other: &SpanStyle) -> SpanStyle {
192        let (merged_color, merged_brush) = merge_foreground_style(self, other);
193        SpanStyle {
194            color: merged_color,
195            brush: merged_brush,
196            alpha: other.alpha.or(self.alpha),
197            font_size: merge_text_unit(self.font_size, other.font_size),
198            font_weight: other.font_weight.or(self.font_weight),
199            font_style: other.font_style.or(self.font_style),
200            font_synthesis: other.font_synthesis.or(self.font_synthesis),
201            font_family: other.font_family.clone().or(self.font_family.clone()),
202            font_feature_settings: other
203                .font_feature_settings
204                .clone()
205                .or(self.font_feature_settings.clone()),
206            letter_spacing: merge_text_unit(self.letter_spacing, other.letter_spacing),
207            baseline_shift: other.baseline_shift.or(self.baseline_shift),
208            text_geometric_transform: other
209                .text_geometric_transform
210                .or(self.text_geometric_transform),
211            locale_list: other.locale_list.clone().or(self.locale_list.clone()),
212            background: other.background.or(self.background),
213            text_decoration: other.text_decoration.or(self.text_decoration),
214            shadow: other.shadow.or(self.shadow),
215            platform_style: other.platform_style.or(self.platform_style),
216            draw_style: other.draw_style.or(self.draw_style),
217        }
218    }
219
220    pub fn plus(&self, other: &SpanStyle) -> SpanStyle {
221        self.merge(other)
222    }
223
224    pub fn resolve_font_size(&self, default_size: f32) -> f32 {
225        let fallback = if default_size.is_finite() && default_size > 0.0 {
226            default_size
227        } else {
228            14.0
229        };
230        match self.font_size {
231            TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
232            TextUnit::Em(value) if value.is_finite() && value > 0.0 => value * fallback,
233            _ => fallback,
234        }
235    }
236
237    pub fn resolve_foreground_color(&self, default_color: Color) -> Color {
238        let mut color = self
239            .color
240            .or_else(|| solid_brush_color(self.brush.as_ref()))
241            .unwrap_or(default_color);
242        if let Some(alpha) = self.alpha {
243            color.3 *= alpha.clamp(0.0, 1.0);
244        }
245        color
246    }
247}
248
249#[derive(Clone, Debug, PartialEq)]
250pub struct ParagraphStyle {
251    pub text_align: TextAlign,
252    pub text_direction: TextDirection,
253    pub line_height: TextUnit,
254    pub text_indent: Option<TextIndent>,
255    pub platform_style: Option<PlatformParagraphStyle>,
256    pub line_height_style: Option<LineHeightStyle>,
257    pub line_break: LineBreak,
258    pub hyphens: Hyphens,
259    pub text_motion: Option<TextMotion>,
260}
261
262impl Default for ParagraphStyle {
263    fn default() -> Self {
264        Self {
265            text_align: TextAlign::Unspecified,
266            text_direction: TextDirection::Unspecified,
267            line_height: TextUnit::Unspecified,
268            text_indent: None,
269            platform_style: None,
270            line_height_style: None,
271            line_break: LineBreak::Unspecified,
272            hyphens: Hyphens::Unspecified,
273            text_motion: None,
274        }
275    }
276}
277
278impl ParagraphStyle {
279    pub fn merge(&self, other: &ParagraphStyle) -> ParagraphStyle {
280        ParagraphStyle {
281            text_align: merge_text_align(self.text_align, other.text_align),
282            text_direction: merge_text_direction(self.text_direction, other.text_direction),
283            line_height: merge_text_unit(self.line_height, other.line_height),
284            text_indent: other.text_indent.or(self.text_indent),
285            platform_style: other.platform_style.or(self.platform_style),
286            line_height_style: other.line_height_style.or(self.line_height_style),
287            line_break: merge_line_break(self.line_break, other.line_break),
288            hyphens: merge_hyphens(self.hyphens, other.hyphens),
289            text_motion: other.text_motion.or(self.text_motion),
290        }
291    }
292
293    pub fn plus(&self, other: &ParagraphStyle) -> ParagraphStyle {
294        self.merge(other)
295    }
296}
297
298#[derive(Clone, Debug, PartialEq, Default)]
299pub struct TextStyle {
300    pub span_style: SpanStyle,
301    pub paragraph_style: ParagraphStyle,
302}
303
304impl TextStyle {
305    pub fn new(span_style: SpanStyle, paragraph_style: ParagraphStyle) -> Self {
306        Self {
307            span_style,
308            paragraph_style,
309        }
310    }
311
312    pub fn from_span_style(span_style: SpanStyle) -> Self {
313        Self::new(span_style, ParagraphStyle::default())
314    }
315
316    pub fn from_paragraph_style(paragraph_style: ParagraphStyle) -> Self {
317        Self::new(SpanStyle::default(), paragraph_style)
318    }
319
320    pub fn merge(&self, other: &TextStyle) -> TextStyle {
321        TextStyle {
322            span_style: self.span_style.merge(&other.span_style),
323            paragraph_style: self.paragraph_style.merge(&other.paragraph_style),
324        }
325    }
326
327    pub fn plus(&self, other: &TextStyle) -> TextStyle {
328        self.merge(other)
329    }
330
331    pub fn to_span_style(&self) -> SpanStyle {
332        self.span_style.clone()
333    }
334
335    pub fn to_paragraph_style(&self) -> ParagraphStyle {
336        self.paragraph_style.clone()
337    }
338
339    pub fn platform_style(&self) -> Option<PlatformTextStyle> {
340        create_platform_text_style(
341            None,
342            self.span_style.platform_style,
343            self.paragraph_style.platform_style,
344        )
345    }
346
347    pub fn with_platform_style(mut self, platform_style: Option<PlatformTextStyle>) -> Self {
348        self.span_style.platform_style = platform_style.and_then(|style| style.span_style);
349        self.paragraph_style.platform_style =
350            platform_style.and_then(|style| style.paragraph_style);
351        self
352    }
353
354    pub fn resolve_font_size(&self, default_size: f32) -> f32 {
355        self.span_style.resolve_font_size(default_size)
356    }
357
358    pub fn resolve_line_height(&self, default_size: f32, natural_line_height: f32) -> f32 {
359        let fallback = if natural_line_height.is_finite() && natural_line_height > 0.0 {
360            natural_line_height
361        } else {
362            self.resolve_font_size(default_size)
363        };
364        match self.paragraph_style.line_height {
365            TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
366            TextUnit::Em(value) if value.is_finite() && value > 0.0 => {
367                value * self.resolve_font_size(default_size)
368            }
369            _ => fallback,
370        }
371    }
372
373    pub fn resolve_letter_spacing(&self, default_size: f32) -> f32 {
374        let font_size = self.resolve_font_size(default_size);
375        match self.span_style.letter_spacing {
376            TextUnit::Sp(value) if value.is_finite() => value,
377            TextUnit::Em(value) if value.is_finite() => value * font_size,
378            _ => 0.0,
379        }
380    }
381
382    pub fn resolve_text_color(&self, default_color: Color) -> Color {
383        self.span_style.resolve_foreground_color(default_color)
384    }
385
386    pub fn measurement_hash(&self) -> u64 {
387        let mut hasher = DefaultHasher::new();
388        let span = &self.span_style;
389        let paragraph = &self.paragraph_style;
390
391        hash_text_unit(span.font_size, &mut hasher);
392        span.font_weight.hash(&mut hasher);
393        span.font_style.hash(&mut hasher);
394        span.font_synthesis.hash(&mut hasher);
395        span.font_family.hash(&mut hasher);
396        span.font_feature_settings.hash(&mut hasher);
397        hash_text_unit(span.letter_spacing, &mut hasher);
398        hash_option_baseline_shift(&span.baseline_shift, &mut hasher);
399        hash_option_geometric_transform(&span.text_geometric_transform, &mut hasher);
400        span.locale_list.hash(&mut hasher);
401        span.platform_style.hash(&mut hasher);
402
403        paragraph.text_align.hash(&mut hasher);
404        paragraph.text_direction.hash(&mut hasher);
405        hash_text_unit(paragraph.line_height, &mut hasher);
406        hash_option_text_indent(&paragraph.text_indent, &mut hasher);
407        paragraph.platform_style.hash(&mut hasher);
408        paragraph.line_height_style.hash(&mut hasher);
409        paragraph.line_break.hash(&mut hasher);
410        paragraph.hyphens.hash(&mut hasher);
411        paragraph.text_motion.hash(&mut hasher);
412
413        hasher.finish()
414    }
415}
416
417fn merge_foreground_style(
418    current: &SpanStyle,
419    incoming: &SpanStyle,
420) -> (Option<Color>, Option<Brush>) {
421    if let Some(brush) = incoming.brush.clone() {
422        return (None, Some(brush));
423    }
424    if let Some(color) = incoming.color {
425        return (Some(color), None);
426    }
427    (current.color, current.brush.clone())
428}
429
430fn solid_brush_color(brush: Option<&Brush>) -> Option<Color> {
431    match brush {
432        Some(Brush::Solid(color)) => Some(*color),
433        _ => None,
434    }
435}
436
437fn create_platform_text_style(
438    explicit: Option<PlatformTextStyle>,
439    span_style: Option<PlatformSpanStyle>,
440    paragraph_style: Option<PlatformParagraphStyle>,
441) -> Option<PlatformTextStyle> {
442    let explicit_span = explicit.and_then(|style| style.span_style);
443    let explicit_paragraph = explicit.and_then(|style| style.paragraph_style);
444    let span = span_style.or(explicit_span);
445    let paragraph = paragraph_style.or(explicit_paragraph);
446    if span.is_none() && paragraph.is_none() {
447        None
448    } else {
449        Some(PlatformTextStyle {
450            span_style: span,
451            paragraph_style: paragraph,
452        })
453    }
454}
455
456fn merge_text_unit(current: TextUnit, incoming: TextUnit) -> TextUnit {
457    if matches!(incoming, TextUnit::Unspecified) {
458        current
459    } else {
460        incoming
461    }
462}
463
464fn merge_text_align(current: TextAlign, incoming: TextAlign) -> TextAlign {
465    if matches!(incoming, TextAlign::Unspecified) {
466        current
467    } else {
468        incoming
469    }
470}
471
472fn merge_text_direction(current: TextDirection, incoming: TextDirection) -> TextDirection {
473    if matches!(incoming, TextDirection::Unspecified) {
474        current
475    } else {
476        incoming
477    }
478}
479
480fn merge_line_break(current: LineBreak, incoming: LineBreak) -> LineBreak {
481    if matches!(incoming, LineBreak::Unspecified) {
482        current
483    } else {
484        incoming
485    }
486}
487
488fn merge_hyphens(current: Hyphens, incoming: Hyphens) -> Hyphens {
489    if matches!(incoming, Hyphens::Unspecified) {
490        current
491    } else {
492        incoming
493    }
494}
495
496fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
497    value.to_bits().hash(state);
498}
499
500fn hash_text_unit<H: Hasher>(unit: TextUnit, state: &mut H) {
501    match unit {
502        TextUnit::Unspecified => 0u8.hash(state),
503        TextUnit::Sp(value) => {
504            1u8.hash(state);
505            hash_f32_bits(value, state);
506        }
507        TextUnit::Em(value) => {
508            2u8.hash(state);
509            hash_f32_bits(value, state);
510        }
511    }
512}
513
514fn hash_option_baseline_shift<H: Hasher>(shift: &Option<BaselineShift>, state: &mut H) {
515    match shift {
516        Some(shift) => {
517            1u8.hash(state);
518            hash_f32_bits(shift.0, state);
519        }
520        None => 0u8.hash(state),
521    }
522}
523
524fn hash_option_geometric_transform<H: Hasher>(
525    transform: &Option<TextGeometricTransform>,
526    state: &mut H,
527) {
528    match transform {
529        Some(transform) => {
530            1u8.hash(state);
531            hash_f32_bits(transform.scale_x, state);
532            hash_f32_bits(transform.skew_x, state);
533        }
534        None => 0u8.hash(state),
535    }
536}
537
538fn hash_option_text_indent<H: Hasher>(indent: &Option<TextIndent>, state: &mut H) {
539    match indent {
540        Some(indent) => {
541            1u8.hash(state);
542            hash_text_unit(indent.first_line, state);
543            hash_text_unit(indent.rest_line, state);
544        }
545        None => 0u8.hash(state),
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use crate::modifier::Brush;
553    use crate::text::{FontFamily, TextDirection};
554
555    #[test]
556    fn baseline_shift_reports_specified() {
557        assert!(BaselineShift::SUPERSCRIPT.is_specified());
558        assert!(!BaselineShift::UNSPECIFIED.is_specified());
559    }
560
561    #[test]
562    fn locale_list_parses_language_tags() {
563        let locale_list = LocaleList::from_language_tags("en-US, ar-EG, ja-JP");
564        assert_eq!(locale_list.locales(), &["en-US", "ar-EG", "ja-JP"]);
565    }
566
567    #[test]
568    fn span_style_merge_prefers_incoming_specified_values() {
569        let base = SpanStyle {
570            font_size: TextUnit::Sp(14.0),
571            font_family: Some(FontFamily::Serif),
572            ..Default::default()
573        };
574        let incoming = SpanStyle {
575            font_size: TextUnit::Unspecified,
576            letter_spacing: TextUnit::Em(0.1),
577            ..Default::default()
578        };
579
580        let merged = base.merge(&incoming);
581        assert_eq!(merged.font_size, TextUnit::Sp(14.0));
582        assert_eq!(merged.letter_spacing, TextUnit::Em(0.1));
583        assert_eq!(merged.font_family, Some(FontFamily::Serif));
584    }
585
586    #[test]
587    fn span_style_merge_switches_foreground_kind() {
588        let base = SpanStyle {
589            color: Some(Color(1.0, 0.0, 0.0, 1.0)),
590            ..Default::default()
591        };
592        let incoming = SpanStyle {
593            brush: Some(Brush::solid(Color(0.0, 1.0, 0.0, 1.0))),
594            ..Default::default()
595        };
596
597        let merged = base.merge(&incoming);
598        assert_eq!(merged.color, None);
599        assert_eq!(merged.brush, incoming.brush);
600    }
601
602    #[test]
603    fn span_style_plus_matches_merge() {
604        let base = SpanStyle {
605            font_size: TextUnit::Sp(12.0),
606            ..Default::default()
607        };
608        let incoming = SpanStyle {
609            letter_spacing: TextUnit::Em(0.2),
610            ..Default::default()
611        };
612        assert_eq!(base.plus(&incoming), base.merge(&incoming));
613    }
614
615    #[test]
616    fn paragraph_style_merge_prefers_specified_values() {
617        let base = ParagraphStyle {
618            text_direction: TextDirection::Ltr,
619            line_height: TextUnit::Sp(18.0),
620            ..Default::default()
621        };
622        let incoming = ParagraphStyle {
623            text_direction: TextDirection::Unspecified,
624            line_height: TextUnit::Em(1.4),
625            ..Default::default()
626        };
627
628        let merged = base.merge(&incoming);
629        assert_eq!(merged.text_direction, TextDirection::Ltr);
630        assert_eq!(merged.line_height, TextUnit::Em(1.4));
631    }
632
633    #[test]
634    fn paragraph_style_plus_matches_merge() {
635        let base = ParagraphStyle {
636            text_align: TextAlign::Start,
637            ..Default::default()
638        };
639        let incoming = ParagraphStyle {
640            text_direction: TextDirection::Rtl,
641            ..Default::default()
642        };
643        assert_eq!(base.plus(&incoming), base.merge(&incoming));
644    }
645
646    #[test]
647    fn resolve_font_size_uses_specified_value() {
648        let style = TextStyle::new(
649            SpanStyle {
650                font_size: TextUnit::Sp(18.0),
651                ..Default::default()
652            },
653            ParagraphStyle::default(),
654        );
655        assert_eq!(style.resolve_font_size(14.0), 18.0);
656    }
657
658    #[test]
659    fn resolve_font_size_handles_em_units() {
660        let style = TextStyle::new(
661            SpanStyle {
662                font_size: TextUnit::Em(1.5),
663                ..Default::default()
664            },
665            ParagraphStyle::default(),
666        );
667        assert_eq!(style.resolve_font_size(16.0), 24.0);
668    }
669
670    #[test]
671    fn resolve_line_height_uses_style_value() {
672        let style = TextStyle::new(
673            SpanStyle {
674                font_size: TextUnit::Sp(20.0),
675                ..Default::default()
676            },
677            ParagraphStyle {
678                line_height: TextUnit::Em(1.2),
679                ..Default::default()
680            },
681        );
682        assert_eq!(style.resolve_line_height(14.0, 18.0), 24.0);
683    }
684
685    #[test]
686    fn resolve_foreground_color_supports_solid_brush_with_alpha() {
687        let style = SpanStyle {
688            brush: Some(Brush::solid(Color(0.2, 0.4, 0.6, 1.0))),
689            alpha: Some(0.5),
690            ..Default::default()
691        };
692        assert_eq!(
693            style.resolve_foreground_color(Color(1.0, 1.0, 1.0, 1.0)),
694            Color(0.2, 0.4, 0.6, 0.5)
695        );
696    }
697
698    #[test]
699    fn resolve_foreground_color_keeps_default_color_for_gradient_brush() {
700        let style = SpanStyle {
701            brush: Some(Brush::linear_gradient(vec![
702                Color(0.1, 0.2, 0.3, 1.0),
703                Color(0.9, 0.8, 0.7, 1.0),
704            ])),
705            alpha: Some(0.25),
706            ..Default::default()
707        };
708
709        assert_eq!(
710            style.resolve_foreground_color(Color(1.0, 1.0, 1.0, 1.0)),
711            Color(1.0, 1.0, 1.0, 0.25)
712        );
713    }
714
715    #[test]
716    fn text_style_merge_combines_span_and_paragraph() {
717        let base = TextStyle::new(
718            SpanStyle {
719                font_family: Some(FontFamily::SansSerif),
720                ..Default::default()
721            },
722            ParagraphStyle {
723                text_direction: TextDirection::Ltr,
724                ..Default::default()
725            },
726        );
727        let incoming = TextStyle::new(
728            SpanStyle {
729                letter_spacing: TextUnit::Em(0.2),
730                ..Default::default()
731            },
732            ParagraphStyle {
733                line_height: TextUnit::Sp(22.0),
734                ..Default::default()
735            },
736        );
737
738        let merged = base.merge(&incoming);
739        assert_eq!(merged.span_style.font_family, Some(FontFamily::SansSerif));
740        assert_eq!(merged.span_style.letter_spacing, TextUnit::Em(0.2));
741        assert_eq!(merged.paragraph_style.text_direction, TextDirection::Ltr);
742        assert_eq!(merged.paragraph_style.line_height, TextUnit::Sp(22.0));
743    }
744
745    #[test]
746    fn text_style_from_and_to_style_helpers_work() {
747        let span_style = SpanStyle {
748            font_size: TextUnit::Sp(12.0),
749            ..Default::default()
750        };
751        let from_span = TextStyle::from_span_style(span_style.clone());
752        assert_eq!(from_span.to_span_style(), span_style);
753
754        let paragraph_style = ParagraphStyle {
755            text_direction: TextDirection::Rtl,
756            ..Default::default()
757        };
758        let from_paragraph = TextStyle::from_paragraph_style(paragraph_style.clone());
759        assert_eq!(from_paragraph.to_paragraph_style(), paragraph_style);
760    }
761
762    #[test]
763    fn text_style_plus_matches_merge() {
764        let base = TextStyle::from_span_style(SpanStyle {
765            font_size: TextUnit::Sp(10.0),
766            ..Default::default()
767        });
768        let incoming = TextStyle::from_paragraph_style(ParagraphStyle {
769            text_direction: TextDirection::Ltr,
770            ..Default::default()
771        });
772        assert_eq!(base.plus(&incoming), base.merge(&incoming));
773    }
774
775    #[test]
776    fn text_style_platform_style_helpers_roundtrip() {
777        let style = TextStyle::default().with_platform_style(Some(PlatformTextStyle {
778            span_style: Some(PlatformSpanStyle),
779            paragraph_style: Some(PlatformParagraphStyle {
780                include_font_padding: Some(false),
781            }),
782        }));
783        assert_eq!(
784            style.platform_style(),
785            Some(PlatformTextStyle {
786                span_style: Some(PlatformSpanStyle),
787                paragraph_style: Some(PlatformParagraphStyle {
788                    include_font_padding: Some(false),
789                }),
790            })
791        );
792    }
793
794    #[test]
795    fn measurement_hash_changes_when_measurement_attributes_change() {
796        let style_a = TextStyle::default();
797        let style_b = TextStyle::new(
798            SpanStyle {
799                font_family: Some(FontFamily::SansSerif),
800                ..Default::default()
801            },
802            ParagraphStyle {
803                text_direction: TextDirection::Rtl,
804                ..Default::default()
805            },
806        );
807
808        assert_ne!(style_a.measurement_hash(), style_b.measurement_hash());
809    }
810
811    #[test]
812    fn measurement_hash_includes_platform_style() {
813        let style_a = TextStyle::default();
814        let style_b = TextStyle::new(
815            SpanStyle {
816                platform_style: Some(PlatformSpanStyle),
817                ..Default::default()
818            },
819            ParagraphStyle::default(),
820        );
821        assert_ne!(style_a.measurement_hash(), style_b.measurement_hash());
822    }
823}