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