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
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 pub fn render_hash(&self) -> u64 {
253 let mut hasher = FxHasher::default();
254 hash_span_style(self, &mut hasher);
255 hasher.finish()
256 }
257}
258
259#[derive(Clone, Debug, PartialEq)]
260pub struct ParagraphStyle {
261 pub text_align: TextAlign,
262 pub text_direction: TextDirection,
263 pub line_height: TextUnit,
264 pub text_indent: Option<TextIndent>,
265 pub platform_style: Option<PlatformParagraphStyle>,
266 pub line_height_style: Option<LineHeightStyle>,
267 pub line_break: LineBreak,
268 pub hyphens: Hyphens,
269 pub text_motion: Option<TextMotion>,
270}
271
272impl Default for ParagraphStyle {
273 fn default() -> Self {
274 Self {
275 text_align: TextAlign::Unspecified,
276 text_direction: TextDirection::Unspecified,
277 line_height: TextUnit::Unspecified,
278 text_indent: None,
279 platform_style: None,
280 line_height_style: None,
281 line_break: LineBreak::Unspecified,
282 hyphens: Hyphens::Unspecified,
283 text_motion: None,
284 }
285 }
286}
287
288impl ParagraphStyle {
289 pub fn merge(&self, other: &ParagraphStyle) -> ParagraphStyle {
290 ParagraphStyle {
291 text_align: merge_text_align(self.text_align, other.text_align),
292 text_direction: merge_text_direction(self.text_direction, other.text_direction),
293 line_height: merge_text_unit(self.line_height, other.line_height),
294 text_indent: other.text_indent.or(self.text_indent),
295 platform_style: other.platform_style.or(self.platform_style),
296 line_height_style: other.line_height_style.or(self.line_height_style),
297 line_break: merge_line_break(self.line_break, other.line_break),
298 hyphens: merge_hyphens(self.hyphens, other.hyphens),
299 text_motion: other.text_motion.or(self.text_motion),
300 }
301 }
302
303 pub fn plus(&self, other: &ParagraphStyle) -> ParagraphStyle {
304 self.merge(other)
305 }
306
307 pub fn render_hash(&self) -> u64 {
308 let mut hasher = FxHasher::default();
309 hash_paragraph_style(self, &mut hasher);
310 hasher.finish()
311 }
312}
313
314#[derive(Clone, Debug, PartialEq, Default)]
315pub struct TextStyle {
316 pub span_style: SpanStyle,
317 pub paragraph_style: ParagraphStyle,
318}
319
320impl TextStyle {
321 pub fn new(span_style: SpanStyle, paragraph_style: ParagraphStyle) -> Self {
322 Self {
323 span_style,
324 paragraph_style,
325 }
326 }
327
328 pub fn from_span_style(span_style: SpanStyle) -> Self {
329 Self::new(span_style, ParagraphStyle::default())
330 }
331
332 pub fn from_paragraph_style(paragraph_style: ParagraphStyle) -> Self {
333 Self::new(SpanStyle::default(), paragraph_style)
334 }
335
336 pub fn merge(&self, other: &TextStyle) -> TextStyle {
337 TextStyle {
338 span_style: self.span_style.merge(&other.span_style),
339 paragraph_style: self.paragraph_style.merge(&other.paragraph_style),
340 }
341 }
342
343 pub fn plus(&self, other: &TextStyle) -> TextStyle {
344 self.merge(other)
345 }
346
347 pub fn to_span_style(&self) -> SpanStyle {
348 self.span_style.clone()
349 }
350
351 pub fn to_paragraph_style(&self) -> ParagraphStyle {
352 self.paragraph_style.clone()
353 }
354
355 pub fn platform_style(&self) -> Option<PlatformTextStyle> {
356 create_platform_text_style(
357 None,
358 self.span_style.platform_style,
359 self.paragraph_style.platform_style,
360 )
361 }
362
363 pub fn with_platform_style(mut self, platform_style: Option<PlatformTextStyle>) -> Self {
364 self.span_style.platform_style = platform_style.and_then(|style| style.span_style);
365 self.paragraph_style.platform_style =
366 platform_style.and_then(|style| style.paragraph_style);
367 self
368 }
369
370 pub fn resolve_font_size(&self, default_size: f32) -> f32 {
371 self.span_style.resolve_font_size(default_size)
372 }
373
374 pub fn resolve_line_height(&self, default_size: f32, natural_line_height: f32) -> f32 {
375 let fallback = if natural_line_height.is_finite() && natural_line_height > 0.0 {
376 natural_line_height
377 } else {
378 self.resolve_font_size(default_size)
379 };
380 match self.paragraph_style.line_height {
381 TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
382 TextUnit::Em(value) if value.is_finite() && value > 0.0 => {
383 value * self.resolve_font_size(default_size)
384 }
385 _ => fallback,
386 }
387 }
388
389 pub fn resolve_letter_spacing(&self, default_size: f32) -> f32 {
390 let font_size = self.resolve_font_size(default_size);
391 match self.span_style.letter_spacing {
392 TextUnit::Sp(value) if value.is_finite() => value,
393 TextUnit::Em(value) if value.is_finite() => value * font_size,
394 _ => 0.0,
395 }
396 }
397
398 pub fn resolve_text_color(&self, default_color: Color) -> Color {
399 self.span_style.resolve_foreground_color(default_color)
400 }
401
402 pub fn measurement_hash(&self) -> u64 {
403 let mut hasher = FxHasher::default();
404 let span = &self.span_style;
405 let paragraph = &self.paragraph_style;
406
407 hash_text_unit(span.font_size, &mut hasher);
408 span.font_weight.hash(&mut hasher);
409 span.font_style.hash(&mut hasher);
410 span.font_synthesis.hash(&mut hasher);
411 span.font_family.hash(&mut hasher);
412 span.font_feature_settings.hash(&mut hasher);
413 hash_text_unit(span.letter_spacing, &mut hasher);
414 hash_option_baseline_shift(&span.baseline_shift, &mut hasher);
415 hash_option_geometric_transform(&span.text_geometric_transform, &mut hasher);
416 span.locale_list.hash(&mut hasher);
417 span.platform_style.hash(&mut hasher);
418
419 paragraph.text_align.hash(&mut hasher);
420 paragraph.text_direction.hash(&mut hasher);
421 hash_text_unit(paragraph.line_height, &mut hasher);
422 hash_option_text_indent(¶graph.text_indent, &mut hasher);
423 paragraph.platform_style.hash(&mut hasher);
424 paragraph.line_height_style.hash(&mut hasher);
425 paragraph.line_break.hash(&mut hasher);
426 paragraph.hyphens.hash(&mut hasher);
427 paragraph.text_motion.hash(&mut hasher);
428
429 hasher.finish()
430 }
431
432 pub fn raster_hash(&self) -> u64 {
436 let mut hasher = FxHasher::default();
437 self.measurement_hash().hash(&mut hasher);
438 let span = &self.span_style;
439 if let Some(color) = span.color {
440 color.0.to_bits().hash(&mut hasher);
441 color.1.to_bits().hash(&mut hasher);
442 color.2.to_bits().hash(&mut hasher);
443 color.3.to_bits().hash(&mut hasher);
444 }
445 if let Some(brush) = &span.brush {
446 format_brush_bits(brush, &mut hasher);
447 }
448 if let Some(alpha) = span.alpha {
449 alpha.to_bits().hash(&mut hasher);
450 }
451 span.text_decoration.hash(&mut hasher);
452 if let Some(shadow) = &span.shadow {
453 shadow.color.0.to_bits().hash(&mut hasher);
454 shadow.color.1.to_bits().hash(&mut hasher);
455 shadow.color.2.to_bits().hash(&mut hasher);
456 shadow.color.3.to_bits().hash(&mut hasher);
457 shadow.offset.x.to_bits().hash(&mut hasher);
458 shadow.offset.y.to_bits().hash(&mut hasher);
459 shadow.blur_radius.to_bits().hash(&mut hasher);
460 }
461 if let Some(draw_style) = &span.draw_style {
462 format!("{draw_style:?}").hash(&mut hasher);
463 }
464 hasher.finish()
465 }
466
467 pub fn render_hash(&self) -> u64 {
468 let mut hasher = FxHasher::default();
469 hash_span_style(&self.span_style, &mut hasher);
470 hash_paragraph_style(&self.paragraph_style, &mut hasher);
471 hasher.finish()
472 }
473}
474
475fn merge_foreground_style(
476 current: &SpanStyle,
477 incoming: &SpanStyle,
478) -> (Option<Color>, Option<Brush>) {
479 if let Some(brush) = incoming.brush.clone() {
480 return (None, Some(brush));
481 }
482 if let Some(color) = incoming.color {
483 return (Some(color), None);
484 }
485 (current.color, current.brush.clone())
486}
487
488fn solid_brush_color(brush: Option<&Brush>) -> Option<Color> {
489 match brush {
490 Some(Brush::Solid(color)) => Some(*color),
491 _ => None,
492 }
493}
494
495fn create_platform_text_style(
496 explicit: Option<PlatformTextStyle>,
497 span_style: Option<PlatformSpanStyle>,
498 paragraph_style: Option<PlatformParagraphStyle>,
499) -> Option<PlatformTextStyle> {
500 let explicit_span = explicit.and_then(|style| style.span_style);
501 let explicit_paragraph = explicit.and_then(|style| style.paragraph_style);
502 let span = span_style.or(explicit_span);
503 let paragraph = paragraph_style.or(explicit_paragraph);
504 if span.is_none() && paragraph.is_none() {
505 None
506 } else {
507 Some(PlatformTextStyle {
508 span_style: span,
509 paragraph_style: paragraph,
510 })
511 }
512}
513
514fn merge_text_unit(current: TextUnit, incoming: TextUnit) -> TextUnit {
515 if matches!(incoming, TextUnit::Unspecified) {
516 current
517 } else {
518 incoming
519 }
520}
521
522fn merge_text_align(current: TextAlign, incoming: TextAlign) -> TextAlign {
523 if matches!(incoming, TextAlign::Unspecified) {
524 current
525 } else {
526 incoming
527 }
528}
529
530fn merge_text_direction(current: TextDirection, incoming: TextDirection) -> TextDirection {
531 if matches!(incoming, TextDirection::Unspecified) {
532 current
533 } else {
534 incoming
535 }
536}
537
538fn merge_line_break(current: LineBreak, incoming: LineBreak) -> LineBreak {
539 if matches!(incoming, LineBreak::Unspecified) {
540 current
541 } else {
542 incoming
543 }
544}
545
546fn merge_hyphens(current: Hyphens, incoming: Hyphens) -> Hyphens {
547 if matches!(incoming, Hyphens::Unspecified) {
548 current
549 } else {
550 incoming
551 }
552}
553
554fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
555 value.to_bits().hash(state);
556}
557
558fn hash_option_color<H: Hasher>(color: &Option<Color>, state: &mut H) {
559 match color {
560 Some(color) => {
561 1u8.hash(state);
562 color.render_hash().hash(state);
563 }
564 None => 0u8.hash(state),
565 }
566}
567
568fn hash_option_brush<H: Hasher>(brush: &Option<Brush>, state: &mut H) {
569 match brush {
570 Some(brush) => {
571 1u8.hash(state);
572 brush.render_hash().hash(state);
573 }
574 None => 0u8.hash(state),
575 }
576}
577
578fn hash_option_alpha<H: Hasher>(alpha: &Option<f32>, state: &mut H) {
579 match alpha {
580 Some(alpha) => {
581 1u8.hash(state);
582 hash_f32_bits(*alpha, state);
583 }
584 None => 0u8.hash(state),
585 }
586}
587
588fn format_brush_bits<H: Hasher>(brush: &cranpose_ui_graphics::Brush, hasher: &mut H) {
589 std::mem::discriminant(brush).hash(hasher);
591 let debug = format!("{brush:?}");
592 debug.hash(hasher);
593}
594
595fn hash_text_unit<H: Hasher>(unit: TextUnit, state: &mut H) {
596 match unit {
597 TextUnit::Unspecified => 0u8.hash(state),
598 TextUnit::Sp(value) => {
599 1u8.hash(state);
600 hash_f32_bits(value, state);
601 }
602 TextUnit::Em(value) => {
603 2u8.hash(state);
604 hash_f32_bits(value, state);
605 }
606 }
607}
608
609fn hash_option_baseline_shift<H: Hasher>(shift: &Option<BaselineShift>, state: &mut H) {
610 match shift {
611 Some(shift) => {
612 1u8.hash(state);
613 hash_f32_bits(shift.0, state);
614 }
615 None => 0u8.hash(state),
616 }
617}
618
619fn hash_option_geometric_transform<H: Hasher>(
620 transform: &Option<TextGeometricTransform>,
621 state: &mut H,
622) {
623 match transform {
624 Some(transform) => {
625 1u8.hash(state);
626 hash_f32_bits(transform.scale_x, state);
627 hash_f32_bits(transform.skew_x, state);
628 }
629 None => 0u8.hash(state),
630 }
631}
632
633fn hash_option_text_indent<H: Hasher>(indent: &Option<TextIndent>, state: &mut H) {
634 match indent {
635 Some(indent) => {
636 1u8.hash(state);
637 hash_text_unit(indent.first_line, state);
638 hash_text_unit(indent.rest_line, state);
639 }
640 None => 0u8.hash(state),
641 }
642}
643
644fn hash_option_shadow<H: Hasher>(shadow: &Option<Shadow>, state: &mut H) {
645 match shadow {
646 Some(shadow) => {
647 1u8.hash(state);
648 shadow.color.render_hash().hash(state);
649 hash_f32_bits(shadow.offset.x, state);
650 hash_f32_bits(shadow.offset.y, state);
651 hash_f32_bits(shadow.blur_radius, state);
652 }
653 None => 0u8.hash(state),
654 }
655}
656
657fn hash_option_text_draw_style<H: Hasher>(draw_style: &Option<TextDrawStyle>, state: &mut H) {
658 match draw_style {
659 Some(TextDrawStyle::Fill) => {
660 1u8.hash(state);
661 0u8.hash(state);
662 }
663 Some(TextDrawStyle::Stroke { width }) => {
664 1u8.hash(state);
665 1u8.hash(state);
666 hash_f32_bits(*width, state);
667 }
668 None => 0u8.hash(state),
669 }
670}
671
672fn hash_span_style<H: Hasher>(span: &SpanStyle, state: &mut H) {
673 hash_option_color(&span.color, state);
674 hash_option_brush(&span.brush, state);
675 hash_option_alpha(&span.alpha, state);
676 hash_text_unit(span.font_size, state);
677 span.font_weight.hash(state);
678 span.font_style.hash(state);
679 span.font_synthesis.hash(state);
680 span.font_family.hash(state);
681 span.font_feature_settings.hash(state);
682 hash_text_unit(span.letter_spacing, state);
683 hash_option_baseline_shift(&span.baseline_shift, state);
684 hash_option_geometric_transform(&span.text_geometric_transform, state);
685 span.locale_list.hash(state);
686 hash_option_color(&span.background, state);
687 span.text_decoration.hash(state);
688 hash_option_shadow(&span.shadow, state);
689 span.platform_style.hash(state);
690 hash_option_text_draw_style(&span.draw_style, state);
691}
692
693fn hash_paragraph_style<H: Hasher>(paragraph: &ParagraphStyle, state: &mut H) {
694 paragraph.text_align.hash(state);
695 paragraph.text_direction.hash(state);
696 hash_text_unit(paragraph.line_height, state);
697 hash_option_text_indent(¶graph.text_indent, state);
698 paragraph.platform_style.hash(state);
699 paragraph.line_height_style.hash(state);
700 paragraph.line_break.hash(state);
701 paragraph.hyphens.hash(state);
702 paragraph.text_motion.hash(state);
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use crate::modifier::Brush;
709 use crate::text::{FontFamily, TextDirection};
710
711 #[test]
712 fn baseline_shift_reports_specified() {
713 assert!(BaselineShift::SUPERSCRIPT.is_specified());
714 assert!(!BaselineShift::UNSPECIFIED.is_specified());
715 }
716
717 #[test]
718 fn locale_list_parses_language_tags() {
719 let locale_list = LocaleList::from_language_tags("en-US, ar-EG, ja-JP");
720 assert_eq!(locale_list.locales(), &["en-US", "ar-EG", "ja-JP"]);
721 }
722
723 #[test]
724 fn span_style_merge_prefers_incoming_specified_values() {
725 let base = SpanStyle {
726 font_size: TextUnit::Sp(14.0),
727 font_family: Some(FontFamily::Serif),
728 ..Default::default()
729 };
730 let incoming = SpanStyle {
731 font_size: TextUnit::Unspecified,
732 letter_spacing: TextUnit::Em(0.1),
733 ..Default::default()
734 };
735
736 let merged = base.merge(&incoming);
737 assert_eq!(merged.font_size, TextUnit::Sp(14.0));
738 assert_eq!(merged.letter_spacing, TextUnit::Em(0.1));
739 assert_eq!(merged.font_family, Some(FontFamily::Serif));
740 }
741
742 #[test]
743 fn span_style_merge_switches_foreground_kind() {
744 let base = SpanStyle {
745 color: Some(Color(1.0, 0.0, 0.0, 1.0)),
746 ..Default::default()
747 };
748 let incoming = SpanStyle {
749 brush: Some(Brush::solid(Color(0.0, 1.0, 0.0, 1.0))),
750 ..Default::default()
751 };
752
753 let merged = base.merge(&incoming);
754 assert_eq!(merged.color, None);
755 assert_eq!(merged.brush, incoming.brush);
756 }
757
758 #[test]
759 fn span_style_plus_matches_merge() {
760 let base = SpanStyle {
761 font_size: TextUnit::Sp(12.0),
762 ..Default::default()
763 };
764 let incoming = SpanStyle {
765 letter_spacing: TextUnit::Em(0.2),
766 ..Default::default()
767 };
768 assert_eq!(base.plus(&incoming), base.merge(&incoming));
769 }
770
771 #[test]
772 fn paragraph_style_merge_prefers_specified_values() {
773 let base = ParagraphStyle {
774 text_direction: TextDirection::Ltr,
775 line_height: TextUnit::Sp(18.0),
776 ..Default::default()
777 };
778 let incoming = ParagraphStyle {
779 text_direction: TextDirection::Unspecified,
780 line_height: TextUnit::Em(1.4),
781 ..Default::default()
782 };
783
784 let merged = base.merge(&incoming);
785 assert_eq!(merged.text_direction, TextDirection::Ltr);
786 assert_eq!(merged.line_height, TextUnit::Em(1.4));
787 }
788
789 #[test]
790 fn paragraph_style_plus_matches_merge() {
791 let base = ParagraphStyle {
792 text_align: TextAlign::Start,
793 ..Default::default()
794 };
795 let incoming = ParagraphStyle {
796 text_direction: TextDirection::Rtl,
797 ..Default::default()
798 };
799 assert_eq!(base.plus(&incoming), base.merge(&incoming));
800 }
801
802 #[test]
803 fn resolve_font_size_uses_specified_value() {
804 let style = TextStyle::new(
805 SpanStyle {
806 font_size: TextUnit::Sp(18.0),
807 ..Default::default()
808 },
809 ParagraphStyle::default(),
810 );
811 assert_eq!(style.resolve_font_size(14.0), 18.0);
812 }
813
814 #[test]
815 fn resolve_font_size_handles_em_units() {
816 let style = TextStyle::new(
817 SpanStyle {
818 font_size: TextUnit::Em(1.5),
819 ..Default::default()
820 },
821 ParagraphStyle::default(),
822 );
823 assert_eq!(style.resolve_font_size(16.0), 24.0);
824 }
825
826 #[test]
827 fn resolve_line_height_uses_style_value() {
828 let style = TextStyle::new(
829 SpanStyle {
830 font_size: TextUnit::Sp(20.0),
831 ..Default::default()
832 },
833 ParagraphStyle {
834 line_height: TextUnit::Em(1.2),
835 ..Default::default()
836 },
837 );
838 assert_eq!(style.resolve_line_height(14.0, 18.0), 24.0);
839 }
840
841 #[test]
842 fn resolve_foreground_color_supports_solid_brush_with_alpha() {
843 let style = SpanStyle {
844 brush: Some(Brush::solid(Color(0.2, 0.4, 0.6, 1.0))),
845 alpha: Some(0.5),
846 ..Default::default()
847 };
848 assert_eq!(
849 style.resolve_foreground_color(Color(1.0, 1.0, 1.0, 1.0)),
850 Color(0.2, 0.4, 0.6, 0.5)
851 );
852 }
853
854 #[test]
855 fn resolve_foreground_color_keeps_default_color_for_gradient_brush() {
856 let style = SpanStyle {
857 brush: Some(Brush::linear_gradient(vec![
858 Color(0.1, 0.2, 0.3, 1.0),
859 Color(0.9, 0.8, 0.7, 1.0),
860 ])),
861 alpha: Some(0.25),
862 ..Default::default()
863 };
864
865 assert_eq!(
866 style.resolve_foreground_color(Color(1.0, 1.0, 1.0, 1.0)),
867 Color(1.0, 1.0, 1.0, 0.25)
868 );
869 }
870
871 #[test]
872 fn text_style_merge_combines_span_and_paragraph() {
873 let base = TextStyle::new(
874 SpanStyle {
875 font_family: Some(FontFamily::SansSerif),
876 ..Default::default()
877 },
878 ParagraphStyle {
879 text_direction: TextDirection::Ltr,
880 ..Default::default()
881 },
882 );
883 let incoming = TextStyle::new(
884 SpanStyle {
885 letter_spacing: TextUnit::Em(0.2),
886 ..Default::default()
887 },
888 ParagraphStyle {
889 line_height: TextUnit::Sp(22.0),
890 ..Default::default()
891 },
892 );
893
894 let merged = base.merge(&incoming);
895 assert_eq!(merged.span_style.font_family, Some(FontFamily::SansSerif));
896 assert_eq!(merged.span_style.letter_spacing, TextUnit::Em(0.2));
897 assert_eq!(merged.paragraph_style.text_direction, TextDirection::Ltr);
898 assert_eq!(merged.paragraph_style.line_height, TextUnit::Sp(22.0));
899 }
900
901 #[test]
902 fn text_style_from_and_to_style_helpers_work() {
903 let span_style = SpanStyle {
904 font_size: TextUnit::Sp(12.0),
905 ..Default::default()
906 };
907 let from_span = TextStyle::from_span_style(span_style.clone());
908 assert_eq!(from_span.to_span_style(), span_style);
909
910 let paragraph_style = ParagraphStyle {
911 text_direction: TextDirection::Rtl,
912 ..Default::default()
913 };
914 let from_paragraph = TextStyle::from_paragraph_style(paragraph_style.clone());
915 assert_eq!(from_paragraph.to_paragraph_style(), paragraph_style);
916 }
917
918 #[test]
919 fn text_style_plus_matches_merge() {
920 let base = TextStyle::from_span_style(SpanStyle {
921 font_size: TextUnit::Sp(10.0),
922 ..Default::default()
923 });
924 let incoming = TextStyle::from_paragraph_style(ParagraphStyle {
925 text_direction: TextDirection::Ltr,
926 ..Default::default()
927 });
928 assert_eq!(base.plus(&incoming), base.merge(&incoming));
929 }
930
931 #[test]
932 fn text_style_platform_style_helpers_roundtrip() {
933 let style = TextStyle::default().with_platform_style(Some(PlatformTextStyle {
934 span_style: Some(PlatformSpanStyle),
935 paragraph_style: Some(PlatformParagraphStyle {
936 include_font_padding: Some(false),
937 shaping: Some(TextShaping::Basic),
938 }),
939 }));
940 assert_eq!(
941 style.platform_style(),
942 Some(PlatformTextStyle {
943 span_style: Some(PlatformSpanStyle),
944 paragraph_style: Some(PlatformParagraphStyle {
945 include_font_padding: Some(false),
946 shaping: Some(TextShaping::Basic),
947 }),
948 })
949 );
950 }
951
952 #[test]
953 fn measurement_hash_changes_when_measurement_attributes_change() {
954 let style_a = TextStyle::default();
955 let style_b = TextStyle::new(
956 SpanStyle {
957 font_family: Some(FontFamily::SansSerif),
958 ..Default::default()
959 },
960 ParagraphStyle {
961 text_direction: TextDirection::Rtl,
962 ..Default::default()
963 },
964 );
965
966 assert_ne!(style_a.measurement_hash(), style_b.measurement_hash());
967 }
968
969 #[test]
970 fn measurement_hash_includes_platform_style() {
971 let style_a = TextStyle::default();
972 let style_b = TextStyle::new(
973 SpanStyle {
974 platform_style: Some(PlatformSpanStyle),
975 ..Default::default()
976 },
977 ParagraphStyle::default(),
978 );
979 assert_ne!(style_a.measurement_hash(), style_b.measurement_hash());
980 }
981
982 #[test]
983 fn measurement_hash_includes_platform_paragraph_shaping() {
984 let style_a = TextStyle::default();
985 let style_b = TextStyle::from_paragraph_style(ParagraphStyle {
986 platform_style: Some(PlatformParagraphStyle {
987 include_font_padding: None,
988 shaping: Some(TextShaping::Basic),
989 }),
990 ..Default::default()
991 });
992 assert_ne!(style_a.measurement_hash(), style_b.measurement_hash());
993 }
994
995 #[test]
996 fn span_style_render_hash_changes_for_visual_attributes() {
997 let plain = SpanStyle::default();
998 let decorated = SpanStyle {
999 shadow: Some(Shadow {
1000 color: Color(1.0, 0.0, 0.0, 0.5),
1001 offset: crate::modifier::Point::new(2.0, 3.0),
1002 blur_radius: 4.0,
1003 }),
1004 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
1005 ..Default::default()
1006 };
1007
1008 assert_ne!(plain.render_hash(), decorated.render_hash());
1009 }
1010
1011 #[test]
1012 fn paragraph_style_render_hash_changes_for_paragraph_attributes() {
1013 let base = ParagraphStyle::default();
1014 let aligned = ParagraphStyle {
1015 text_align: TextAlign::Center,
1016 text_direction: TextDirection::Rtl,
1017 ..Default::default()
1018 };
1019
1020 assert_ne!(base.render_hash(), aligned.render_hash());
1021 }
1022
1023 #[test]
1024 fn text_style_render_hash_includes_visual_attributes() {
1025 let base = TextStyle::default();
1026 let tinted = TextStyle::from_span_style(SpanStyle {
1027 color: Some(Color(0.1, 0.2, 0.3, 1.0)),
1028 background: Some(Color(0.9, 0.8, 0.7, 1.0)),
1029 ..Default::default()
1030 });
1031
1032 assert_ne!(base.render_hash(), tinted.render_hash());
1033 }
1034}