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)]
24pub struct BaselineShift(pub f32);
25
26impl BaselineShift {
27 pub const SUPERSCRIPT: Self = Self(0.5);
29 pub const SUBSCRIPT: Self = Self(-0.5);
31 pub const NONE: Self = Self(0.0);
33 pub const UNSPECIFIED: Self = Self(f32::NAN);
35
36 pub fn is_specified(self) -> bool {
38 !self.0.is_nan()
39 }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct TextGeometricTransform {
46 pub scale_x: f32,
48 pub skew_x: f32,
50}
51
52impl Default for TextGeometricTransform {
53 fn default() -> Self {
54 Self {
55 scale_x: 1.0,
56 skew_x: 0.0,
57 }
58 }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
64pub struct LocaleList {
65 locales: std::sync::Arc<[String]>,
66}
67
68impl LocaleList {
69 pub fn new(locales: Vec<String>) -> Self {
71 Self {
72 locales: locales.into(),
73 }
74 }
75
76 pub fn from_language_tags(tags: &str) -> Self {
79 let locales: Vec<_> = tags
80 .split(',')
81 .map(str::trim)
82 .filter(|tag| !tag.is_empty())
83 .map(ToString::to_string)
84 .collect();
85 Self::new(locales)
86 }
87
88 pub fn locales(&self) -> &[String] {
90 &self.locales
91 }
92
93 pub fn is_empty(&self) -> bool {
95 self.locales.is_empty()
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
101pub enum TextMotion {
102 #[default]
104 Static,
105 Animated,
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
112pub struct PlatformSpanStyle;
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
116pub enum TextShaping {
117 Basic,
119 Advanced,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
125pub struct PlatformParagraphStyle {
126 pub include_font_padding: Option<bool>,
129 pub shaping: Option<TextShaping>,
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
135pub struct PlatformTextStyle {
136 pub span_style: Option<PlatformSpanStyle>,
138 pub paragraph_style: Option<PlatformParagraphStyle>,
140}
141
142#[derive(Clone, Copy, Debug, PartialEq, Default)]
144pub enum TextDrawStyle {
145 #[default]
147 Fill,
148 Stroke {
150 width: f32,
152 },
153}
154
155#[derive(Clone, Debug, PartialEq)]
158pub struct SpanStyle {
159 pub color: Option<Color>,
161 pub brush: Option<Brush>,
164 pub alpha: Option<f32>,
166 pub font_size: TextUnit,
168 pub font_weight: Option<FontWeight>,
170 pub font_style: Option<FontStyle>,
172 pub font_synthesis: Option<FontSynthesis>,
174 pub font_family: Option<FontFamily>,
176 pub font_feature_settings: Option<std::sync::Arc<str>>,
178 pub letter_spacing: TextUnit,
180 pub baseline_shift: Option<BaselineShift>,
182 pub text_geometric_transform: Option<TextGeometricTransform>,
184 pub locale_list: Option<LocaleList>,
186 pub background: Option<Color>,
188 pub text_decoration: Option<TextDecoration>,
190 pub shadow: Option<Shadow>,
192 pub platform_style: Option<PlatformSpanStyle>,
194 pub draw_style: Option<TextDrawStyle>,
196}
197
198impl Default for SpanStyle {
199 fn default() -> Self {
200 Self {
201 color: None,
202 brush: None,
203 alpha: None,
204 font_size: TextUnit::Unspecified,
205 font_weight: None,
206 font_style: None,
207 font_synthesis: None,
208 font_family: None,
209 font_feature_settings: None,
210 letter_spacing: TextUnit::Unspecified,
211 baseline_shift: None,
212 text_geometric_transform: None,
213 locale_list: None,
214 background: None,
215 text_decoration: None,
216 shadow: None,
217 platform_style: None,
218 draw_style: None,
219 }
220 }
221}
222
223impl SpanStyle {
224 pub fn merge(&self, other: &SpanStyle) -> SpanStyle {
227 let (merged_color, merged_brush) = merge_foreground_style(self, other);
228 SpanStyle {
229 color: merged_color,
230 brush: merged_brush,
231 alpha: other.alpha.or(self.alpha),
232 font_size: merge_text_unit(self.font_size, other.font_size),
233 font_weight: other.font_weight.or(self.font_weight),
234 font_style: other.font_style.or(self.font_style),
235 font_synthesis: other.font_synthesis.or(self.font_synthesis),
236 font_family: other
237 .font_family
238 .clone()
239 .or_else(|| self.font_family.clone()),
240 font_feature_settings: other
241 .font_feature_settings
242 .clone()
243 .or_else(|| self.font_feature_settings.clone()),
244 letter_spacing: merge_text_unit(self.letter_spacing, other.letter_spacing),
245 baseline_shift: other.baseline_shift.or(self.baseline_shift),
246 text_geometric_transform: other
247 .text_geometric_transform
248 .or(self.text_geometric_transform),
249 locale_list: other
250 .locale_list
251 .clone()
252 .or_else(|| self.locale_list.clone()),
253 background: other.background.or(self.background),
254 text_decoration: other.text_decoration.or(self.text_decoration),
255 shadow: other.shadow.or(self.shadow),
256 platform_style: other.platform_style.or(self.platform_style),
257 draw_style: other.draw_style.or(self.draw_style),
258 }
259 }
260
261 pub fn plus(&self, other: &SpanStyle) -> SpanStyle {
263 self.merge(other)
264 }
265
266 pub fn resolve_font_size(&self, default_size: f32) -> f32 {
270 let fallback = if default_size.is_finite() && default_size > 0.0 {
271 default_size
272 } else {
273 14.0
274 };
275 match self.font_size {
276 TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
277 TextUnit::Em(value) if value.is_finite() && value > 0.0 => value * fallback,
278 _ => fallback,
279 }
280 }
281
282 pub fn resolve_foreground_color(&self, default_color: Color) -> Color {
285 let mut color = self
286 .color
287 .or_else(|| solid_brush_color(self.brush.as_ref()))
288 .unwrap_or(default_color);
289 if let Some(alpha) = self.alpha {
290 color.3 *= alpha.clamp(0.0, 1.0);
291 }
292 color
293 }
294
295 pub fn render_hash(&self) -> u64 {
297 let mut hasher = FxHasher::default();
298 hash_span_style(self, &mut hasher);
299 hasher.finish()
300 }
301}
302
303#[derive(Clone, Debug, PartialEq)]
306pub struct ParagraphStyle {
307 pub text_align: TextAlign,
309 pub text_direction: TextDirection,
311 pub line_height: TextUnit,
314 pub text_indent: Option<TextIndent>,
316 pub platform_style: Option<PlatformParagraphStyle>,
318 pub line_height_style: Option<LineHeightStyle>,
320 pub line_break: LineBreak,
322 pub hyphens: Hyphens,
324 pub text_motion: Option<TextMotion>,
326}
327
328impl Default for ParagraphStyle {
329 fn default() -> Self {
330 Self {
331 text_align: TextAlign::Unspecified,
332 text_direction: TextDirection::Unspecified,
333 line_height: TextUnit::Unspecified,
334 text_indent: None,
335 platform_style: None,
336 line_height_style: None,
337 line_break: LineBreak::Unspecified,
338 hyphens: Hyphens::Unspecified,
339 text_motion: None,
340 }
341 }
342}
343
344impl ParagraphStyle {
345 pub fn merge(&self, other: &ParagraphStyle) -> ParagraphStyle {
348 ParagraphStyle {
349 text_align: merge_text_align(self.text_align, other.text_align),
350 text_direction: merge_text_direction(self.text_direction, other.text_direction),
351 line_height: merge_text_unit(self.line_height, other.line_height),
352 text_indent: other.text_indent.or(self.text_indent),
353 platform_style: other.platform_style.or(self.platform_style),
354 line_height_style: other.line_height_style.or(self.line_height_style),
355 line_break: merge_line_break(self.line_break, other.line_break),
356 hyphens: merge_hyphens(self.hyphens, other.hyphens),
357 text_motion: other.text_motion.or(self.text_motion),
358 }
359 }
360
361 pub fn plus(&self, other: &ParagraphStyle) -> ParagraphStyle {
363 self.merge(other)
364 }
365
366 pub fn render_hash(&self) -> u64 {
369 let mut hasher = FxHasher::default();
370 hash_paragraph_style(self, &mut hasher);
371 hasher.finish()
372 }
373}
374
375#[derive(Clone, Debug, PartialEq, Default)]
377pub struct TextStyle {
378 pub span_style: SpanStyle,
380 pub paragraph_style: ParagraphStyle,
382}
383
384impl TextStyle {
385 pub fn new(span_style: SpanStyle, paragraph_style: ParagraphStyle) -> Self {
387 Self {
388 span_style,
389 paragraph_style,
390 }
391 }
392
393 pub fn from_span_style(span_style: SpanStyle) -> Self {
395 Self::new(span_style, ParagraphStyle::default())
396 }
397
398 pub fn from_paragraph_style(paragraph_style: ParagraphStyle) -> Self {
400 Self::new(SpanStyle::default(), paragraph_style)
401 }
402
403 pub fn merge(&self, other: &TextStyle) -> TextStyle {
406 TextStyle {
407 span_style: self.span_style.merge(&other.span_style),
408 paragraph_style: self.paragraph_style.merge(&other.paragraph_style),
409 }
410 }
411
412 pub fn plus(&self, other: &TextStyle) -> TextStyle {
414 self.merge(other)
415 }
416
417 pub fn to_span_style(&self) -> SpanStyle {
419 self.span_style.clone()
420 }
421
422 pub fn to_paragraph_style(&self) -> ParagraphStyle {
424 self.paragraph_style.clone()
425 }
426
427 pub fn platform_style(&self) -> Option<PlatformTextStyle> {
430 create_platform_text_style(
431 None,
432 self.span_style.platform_style,
433 self.paragraph_style.platform_style,
434 )
435 }
436
437 pub fn with_platform_style(mut self, platform_style: Option<PlatformTextStyle>) -> Self {
440 self.span_style.platform_style = platform_style.and_then(|style| style.span_style);
441 self.paragraph_style.platform_style =
442 platform_style.and_then(|style| style.paragraph_style);
443 self
444 }
445
446 pub fn resolve_font_size(&self, default_size: f32) -> f32 {
448 self.span_style.resolve_font_size(default_size)
449 }
450
451 pub fn resolve_line_height(&self, default_size: f32, natural_line_height: f32) -> f32 {
455 let fallback = if natural_line_height.is_finite() && natural_line_height > 0.0 {
456 natural_line_height
457 } else {
458 self.resolve_font_size(default_size)
459 };
460 match self.paragraph_style.line_height {
461 TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
462 TextUnit::Em(value) if value.is_finite() && value > 0.0 => {
463 value * self.resolve_font_size(default_size)
464 }
465 _ => fallback,
466 }
467 }
468
469 pub fn resolve_letter_spacing(&self, default_size: f32) -> f32 {
472 let font_size = self.resolve_font_size(default_size);
473 match self.span_style.letter_spacing {
474 TextUnit::Sp(value) if value.is_finite() => value,
475 TextUnit::Em(value) if value.is_finite() => value * font_size,
476 _ => 0.0,
477 }
478 }
479
480 pub fn resolve_text_color(&self, default_color: Color) -> Color {
482 self.span_style.resolve_foreground_color(default_color)
483 }
484
485 pub fn measurement_hash(&self) -> u64 {
488 let mut hasher = FxHasher::default();
489 hash_span_measurement(&self.span_style, &mut hasher);
490 hash_paragraph_style(&self.paragraph_style, &mut hasher);
491 hasher.finish()
492 }
493
494 pub fn render_hash(&self) -> u64 {
496 let mut hasher = FxHasher::default();
497 hash_span_style(&self.span_style, &mut hasher);
498 hash_paragraph_style(&self.paragraph_style, &mut hasher);
499 hasher.finish()
500 }
501}
502
503fn merge_foreground_style(
504 current: &SpanStyle,
505 incoming: &SpanStyle,
506) -> (Option<Color>, Option<Brush>) {
507 if let Some(brush) = incoming.brush.clone() {
508 return (None, Some(brush));
509 }
510 if let Some(color) = incoming.color {
511 return (Some(color), None);
512 }
513 (current.color, current.brush.clone())
514}
515
516fn solid_brush_color(brush: Option<&Brush>) -> Option<Color> {
517 match brush {
518 Some(Brush::Solid(color)) => Some(*color),
519 _ => None,
520 }
521}
522
523fn create_platform_text_style(
524 explicit: Option<PlatformTextStyle>,
525 span_style: Option<PlatformSpanStyle>,
526 paragraph_style: Option<PlatformParagraphStyle>,
527) -> Option<PlatformTextStyle> {
528 let explicit_span = explicit.and_then(|style| style.span_style);
529 let explicit_paragraph = explicit.and_then(|style| style.paragraph_style);
530 let span = span_style.or(explicit_span);
531 let paragraph = paragraph_style.or(explicit_paragraph);
532 if span.is_none() && paragraph.is_none() {
533 None
534 } else {
535 Some(PlatformTextStyle {
536 span_style: span,
537 paragraph_style: paragraph,
538 })
539 }
540}
541
542fn merge_text_unit(current: TextUnit, incoming: TextUnit) -> TextUnit {
543 if matches!(incoming, TextUnit::Unspecified) {
544 current
545 } else {
546 incoming
547 }
548}
549
550fn merge_text_align(current: TextAlign, incoming: TextAlign) -> TextAlign {
551 if matches!(incoming, TextAlign::Unspecified) {
552 current
553 } else {
554 incoming
555 }
556}
557
558fn merge_text_direction(current: TextDirection, incoming: TextDirection) -> TextDirection {
559 if matches!(incoming, TextDirection::Unspecified) {
560 current
561 } else {
562 incoming
563 }
564}
565
566fn merge_line_break(current: LineBreak, incoming: LineBreak) -> LineBreak {
567 if matches!(incoming, LineBreak::Unspecified) {
568 current
569 } else {
570 incoming
571 }
572}
573
574fn merge_hyphens(current: Hyphens, incoming: Hyphens) -> Hyphens {
575 if matches!(incoming, Hyphens::Unspecified) {
576 current
577 } else {
578 incoming
579 }
580}
581
582pub(crate) fn hash_span_measurement<H: Hasher>(span: &SpanStyle, state: &mut H) {
585 hash_text_unit(span.font_size, state);
586 span.font_weight.hash(state);
587 span.font_style.hash(state);
588 span.font_synthesis.hash(state);
589 span.font_family.hash(state);
590 span.font_feature_settings.hash(state);
591 hash_text_unit(span.letter_spacing, state);
592 hash_option_baseline_shift(&span.baseline_shift, state);
593 hash_option_geometric_transform(&span.text_geometric_transform, state);
594 span.locale_list.hash(state);
595 span.platform_style.hash(state);
596}
597
598fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
599 value.to_bits().hash(state);
600}
601
602fn hash_option_color<H: Hasher>(color: &Option<Color>, state: &mut H) {
603 match color {
604 Some(color) => {
605 1u8.hash(state);
606 color.render_hash().hash(state);
607 }
608 None => 0u8.hash(state),
609 }
610}
611
612fn hash_option_brush<H: Hasher>(brush: &Option<Brush>, state: &mut H) {
613 match brush {
614 Some(brush) => {
615 1u8.hash(state);
616 brush.render_hash().hash(state);
617 }
618 None => 0u8.hash(state),
619 }
620}
621
622fn hash_option_alpha<H: Hasher>(alpha: &Option<f32>, state: &mut H) {
623 match alpha {
624 Some(alpha) => {
625 1u8.hash(state);
626 hash_f32_bits(*alpha, state);
627 }
628 None => 0u8.hash(state),
629 }
630}
631
632fn hash_text_unit<H: Hasher>(unit: TextUnit, state: &mut H) {
633 match unit {
634 TextUnit::Unspecified => 0u8.hash(state),
635 TextUnit::Sp(value) => {
636 1u8.hash(state);
637 hash_f32_bits(value, state);
638 }
639 TextUnit::Em(value) => {
640 2u8.hash(state);
641 hash_f32_bits(value, state);
642 }
643 }
644}
645
646fn hash_option_baseline_shift<H: Hasher>(shift: &Option<BaselineShift>, state: &mut H) {
647 match shift {
648 Some(shift) => {
649 1u8.hash(state);
650 hash_f32_bits(shift.0, state);
651 }
652 None => 0u8.hash(state),
653 }
654}
655
656fn hash_option_geometric_transform<H: Hasher>(
657 transform: &Option<TextGeometricTransform>,
658 state: &mut H,
659) {
660 match transform {
661 Some(transform) => {
662 1u8.hash(state);
663 hash_f32_bits(transform.scale_x, state);
664 hash_f32_bits(transform.skew_x, state);
665 }
666 None => 0u8.hash(state),
667 }
668}
669
670fn hash_option_text_indent<H: Hasher>(indent: &Option<TextIndent>, state: &mut H) {
671 match indent {
672 Some(indent) => {
673 1u8.hash(state);
674 hash_text_unit(indent.first_line, state);
675 hash_text_unit(indent.rest_line, state);
676 }
677 None => 0u8.hash(state),
678 }
679}
680
681fn hash_option_shadow<H: Hasher>(shadow: &Option<Shadow>, state: &mut H) {
682 match shadow {
683 Some(shadow) => {
684 1u8.hash(state);
685 shadow.color.render_hash().hash(state);
686 hash_f32_bits(shadow.offset.x, state);
687 hash_f32_bits(shadow.offset.y, state);
688 hash_f32_bits(shadow.blur_radius, state);
689 }
690 None => 0u8.hash(state),
691 }
692}
693
694fn hash_option_text_draw_style<H: Hasher>(draw_style: &Option<TextDrawStyle>, state: &mut H) {
695 match draw_style {
696 Some(TextDrawStyle::Fill) => {
697 1u8.hash(state);
698 0u8.hash(state);
699 }
700 Some(TextDrawStyle::Stroke { width }) => {
701 1u8.hash(state);
702 1u8.hash(state);
703 hash_f32_bits(*width, state);
704 }
705 None => 0u8.hash(state),
706 }
707}
708
709fn hash_span_style<H: Hasher>(span: &SpanStyle, state: &mut H) {
710 hash_span_measurement(span, state);
711 hash_option_color(&span.color, state);
712 hash_option_brush(&span.brush, state);
713 hash_option_alpha(&span.alpha, state);
714 hash_option_color(&span.background, state);
715 span.text_decoration.hash(state);
716 hash_option_shadow(&span.shadow, state);
717 hash_option_text_draw_style(&span.draw_style, state);
718}
719
720fn hash_paragraph_style<H: Hasher>(paragraph: &ParagraphStyle, state: &mut H) {
721 paragraph.text_align.hash(state);
722 paragraph.text_direction.hash(state);
723 hash_text_unit(paragraph.line_height, state);
724 hash_option_text_indent(¶graph.text_indent, state);
725 paragraph.platform_style.hash(state);
726 paragraph.line_height_style.hash(state);
727 paragraph.line_break.hash(state);
728 paragraph.hyphens.hash(state);
729 paragraph.text_motion.hash(state);
730}
731
732pub fn text_align_fraction(text_style: &TextStyle, text: &str) -> f32 {
746 let paragraph_style = &text_style.paragraph_style;
747 match paragraph_style.text_align {
748 super::TextAlign::Left => 0.0,
749 super::TextAlign::Right => 1.0,
750 super::TextAlign::Center => 0.5,
751 super::TextAlign::Start
752 | super::TextAlign::End
753 | super::TextAlign::Justify
754 | super::TextAlign::Unspecified => {
755 let direction =
756 super::resolve_text_direction(text, Some(paragraph_style.text_direction));
757 let start = match direction {
758 super::ResolvedTextDirection::Ltr => 0.0,
759 super::ResolvedTextDirection::Rtl => 1.0,
760 };
761 if paragraph_style.text_align == super::TextAlign::End {
762 1.0 - start
763 } else {
764 start
765 }
766 }
767 }
768}
769
770#[cfg(test)]
771#[path = "tests/style_tests.rs"]
772mod tests;