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