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