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