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