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(¶graph.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}