1use std::hash::{Hash, Hasher};
2
3use cranpose_ui_graphics::{Brush, Color, 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};
19
20#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct BaselineShift(pub f32);
24
25impl BaselineShift {
26 pub const SUPERSCRIPT: Self = Self(0.5);
28 pub const SUBSCRIPT: Self = Self(-0.5);
30 pub const NONE: Self = Self(0.0);
32 pub const UNSPECIFIED: Self = Self(f32::NAN);
34
35 pub fn is_specified(self) -> bool {
37 !self.0.is_nan()
38 }
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct TextGeometricTransform {
45 pub scale_x: f32,
47 pub skew_x: f32,
49}
50
51impl Default for TextGeometricTransform {
52 fn default() -> Self {
53 Self {
54 scale_x: 1.0,
55 skew_x: 0.0,
56 }
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
63pub struct LocaleList {
64 locales: std::sync::Arc<[String]>,
65}
66
67impl LocaleList {
68 pub fn new(locales: Vec<String>) -> Self {
70 Self {
71 locales: locales.into(),
72 }
73 }
74
75 pub fn from_language_tags(tags: &str) -> Self {
78 let locales: Vec<_> = tags
79 .split(',')
80 .map(str::trim)
81 .filter(|tag| !tag.is_empty())
82 .map(ToString::to_string)
83 .collect();
84 Self::new(locales)
85 }
86
87 pub fn locales(&self) -> &[String] {
89 &self.locales
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.locales.is_empty()
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
100pub enum TextMotion {
101 #[default]
103 Static,
104 Animated,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
111pub struct PlatformSpanStyle;
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115pub enum TextShaping {
116 Basic,
118 Advanced,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
124pub struct PlatformParagraphStyle {
125 pub include_font_padding: Option<bool>,
128 pub shaping: Option<TextShaping>,
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
134pub struct PlatformTextStyle {
135 pub span_style: Option<PlatformSpanStyle>,
137 pub paragraph_style: Option<PlatformParagraphStyle>,
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Default)]
143pub enum TextDrawStyle {
144 #[default]
146 Fill,
147 Stroke {
149 width: f32,
151 },
152}
153
154#[derive(Clone, Debug, PartialEq)]
157pub struct SpanStyle {
158 pub color: Option<Color>,
160 pub brush: Option<Brush>,
163 pub alpha: Option<f32>,
165 pub font_size: TextUnit,
167 pub font_weight: Option<FontWeight>,
169 pub font_style: Option<FontStyle>,
171 pub font_synthesis: Option<FontSynthesis>,
173 pub font_family: Option<FontFamily>,
175 pub font_feature_settings: Option<crate::font_feature::FontFeatureSettings>,
177 pub letter_spacing: TextUnit,
179 pub baseline_shift: Option<BaselineShift>,
181 pub text_geometric_transform: Option<TextGeometricTransform>,
183 pub locale_list: Option<LocaleList>,
185 pub background: Option<Color>,
187 pub text_decoration: Option<TextDecoration>,
189 pub shadow: Option<Shadow>,
191 pub platform_style: Option<PlatformSpanStyle>,
193 pub draw_style: Option<TextDrawStyle>,
195}
196
197impl Default for SpanStyle {
198 fn default() -> Self {
199 Self {
200 color: None,
201 brush: None,
202 alpha: None,
203 font_size: TextUnit::Unspecified,
204 font_weight: None,
205 font_style: None,
206 font_synthesis: None,
207 font_family: None,
208 font_feature_settings: None,
209 letter_spacing: TextUnit::Unspecified,
210 baseline_shift: None,
211 text_geometric_transform: None,
212 locale_list: None,
213 background: None,
214 text_decoration: None,
215 shadow: None,
216 platform_style: None,
217 draw_style: None,
218 }
219 }
220}
221
222impl SpanStyle {
223 pub fn merge(&self, other: &Self) -> Self {
226 let (merged_color, merged_brush) = merge_foreground_style(self, other);
227 Self {
228 color: merged_color,
229 brush: merged_brush,
230 alpha: other.alpha.or(self.alpha),
231 font_size: merge_text_unit(self.font_size, other.font_size),
232 font_weight: other.font_weight.or(self.font_weight),
233 font_style: other.font_style.or(self.font_style),
234 font_synthesis: other.font_synthesis.or(self.font_synthesis),
235 font_family: other
236 .font_family
237 .clone()
238 .or_else(|| self.font_family.clone()),
239 font_feature_settings: other
240 .font_feature_settings
241 .clone()
242 .or_else(|| self.font_feature_settings.clone()),
243 letter_spacing: merge_text_unit(self.letter_spacing, other.letter_spacing),
244 baseline_shift: other.baseline_shift.or(self.baseline_shift),
245 text_geometric_transform: other
246 .text_geometric_transform
247 .or(self.text_geometric_transform),
248 locale_list: other
249 .locale_list
250 .clone()
251 .or_else(|| self.locale_list.clone()),
252 background: other.background.or(self.background),
253 text_decoration: other.text_decoration.or(self.text_decoration),
254 shadow: other.shadow.or(self.shadow),
255 platform_style: other.platform_style.or(self.platform_style),
256 draw_style: other.draw_style.or(self.draw_style),
257 }
258 }
259
260 pub fn plus(&self, other: &Self) -> Self {
262 self.merge(other)
263 }
264
265 pub fn resolve_font_size(&self, default_size: f32) -> f32 {
269 let fallback = if default_size.is_finite() && default_size > 0.0 {
270 default_size
271 } else {
272 14.0
273 };
274 match self.font_size {
275 TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
276 TextUnit::Em(value) if value.is_finite() && value > 0.0 => value * fallback,
277 _ => fallback,
278 }
279 }
280
281 pub fn resolve_foreground_color(&self, default_color: Color) -> Color {
284 let mut color = self
285 .color
286 .or_else(|| solid_brush_color(self.brush.as_ref()))
287 .unwrap_or(default_color);
288 if let Some(alpha) = self.alpha {
289 color.3 *= alpha.clamp(0.0, 1.0);
290 }
291 color
292 }
293
294 pub fn render_hash(&self) -> u64 {
296 let mut hasher = FxHasher::default();
297 hash_span_style(self, &mut hasher);
298 hasher.finish()
299 }
300}
301
302#[derive(Clone, Debug, PartialEq)]
305pub struct ParagraphStyle {
306 pub text_align: TextAlign,
308 pub text_direction: TextDirection,
310 pub line_height: TextUnit,
313 pub text_indent: Option<TextIndent>,
315 pub platform_style: Option<PlatformParagraphStyle>,
317 pub line_height_style: Option<LineHeightStyle>,
319 pub line_break: LineBreak,
321 pub hyphens: Hyphens,
323 pub text_motion: Option<TextMotion>,
325}
326
327impl Default for ParagraphStyle {
328 fn default() -> Self {
329 Self {
330 text_align: TextAlign::Unspecified,
331 text_direction: TextDirection::Unspecified,
332 line_height: TextUnit::Unspecified,
333 text_indent: None,
334 platform_style: None,
335 line_height_style: None,
336 line_break: LineBreak::Unspecified,
337 hyphens: Hyphens::Unspecified,
338 text_motion: None,
339 }
340 }
341}
342
343impl ParagraphStyle {
344 pub fn merge(&self, other: &Self) -> Self {
347 Self {
348 text_align: merge_text_align(self.text_align, other.text_align),
349 text_direction: merge_text_direction(self.text_direction, other.text_direction),
350 line_height: merge_text_unit(self.line_height, other.line_height),
351 text_indent: other.text_indent.or(self.text_indent),
352 platform_style: other.platform_style.or(self.platform_style),
353 line_height_style: other.line_height_style.or(self.line_height_style),
354 line_break: merge_line_break(self.line_break, other.line_break),
355 hyphens: merge_hyphens(self.hyphens, other.hyphens),
356 text_motion: other.text_motion.or(self.text_motion),
357 }
358 }
359
360 pub fn plus(&self, other: &Self) -> Self {
362 self.merge(other)
363 }
364
365 pub fn render_hash(&self) -> u64 {
368 let mut hasher = FxHasher::default();
369 hash_paragraph_style(self, &mut hasher);
370 hasher.finish()
371 }
372}
373
374#[derive(Clone, Debug, PartialEq, Default)]
376pub struct TextStyle {
377 pub span_style: SpanStyle,
379 pub paragraph_style: ParagraphStyle,
381}
382
383impl TextStyle {
384 pub fn new(span_style: SpanStyle, paragraph_style: ParagraphStyle) -> Self {
386 Self {
387 span_style,
388 paragraph_style,
389 }
390 }
391
392 pub fn from_span_style(span_style: SpanStyle) -> Self {
394 Self::new(span_style, ParagraphStyle::default())
395 }
396
397 pub fn from_paragraph_style(paragraph_style: ParagraphStyle) -> Self {
399 Self::new(SpanStyle::default(), paragraph_style)
400 }
401
402 pub fn merge(&self, other: &Self) -> Self {
405 Self {
406 span_style: self.span_style.merge(&other.span_style),
407 paragraph_style: self.paragraph_style.merge(&other.paragraph_style),
408 }
409 }
410
411 pub fn plus(&self, other: &Self) -> Self {
413 self.merge(other)
414 }
415
416 pub fn to_span_style(&self) -> SpanStyle {
418 self.span_style.clone()
419 }
420
421 pub fn to_paragraph_style(&self) -> ParagraphStyle {
423 self.paragraph_style.clone()
424 }
425
426 pub fn platform_style(&self) -> Option<PlatformTextStyle> {
429 create_platform_text_style(
430 None,
431 self.span_style.platform_style,
432 self.paragraph_style.platform_style,
433 )
434 }
435
436 pub fn with_platform_style(mut self, platform_style: Option<PlatformTextStyle>) -> Self {
439 self.span_style.platform_style = platform_style.and_then(|style| style.span_style);
440 self.paragraph_style.platform_style =
441 platform_style.and_then(|style| style.paragraph_style);
442 self
443 }
444
445 pub fn resolve_font_size(&self, default_size: f32) -> f32 {
447 self.span_style.resolve_font_size(default_size)
448 }
449
450 pub fn resolve_line_height(&self, default_size: f32, natural_line_height: f32) -> f32 {
454 let fallback = if natural_line_height.is_finite() && natural_line_height > 0.0 {
455 natural_line_height
456 } else {
457 self.resolve_font_size(default_size)
458 };
459 match self.paragraph_style.line_height {
460 TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
461 TextUnit::Em(value) if value.is_finite() && value > 0.0 => {
462 value * self.resolve_font_size(default_size)
463 }
464 _ => fallback,
465 }
466 }
467
468 pub fn resolve_letter_spacing(&self, default_size: f32) -> f32 {
471 let font_size = self.resolve_font_size(default_size);
472 match self.span_style.letter_spacing {
473 TextUnit::Sp(value) if value.is_finite() => value,
474 TextUnit::Em(value) if value.is_finite() => value * font_size,
475 _ => 0.0,
476 }
477 }
478
479 pub fn resolve_text_color(&self, default_color: Color) -> Color {
481 self.span_style.resolve_foreground_color(default_color)
482 }
483
484 pub fn measurement_hash(&self) -> u64 {
487 let mut hasher = FxHasher::default();
488 hash_span_measurement(&self.span_style, &mut hasher);
489 hash_paragraph_style(&self.paragraph_style, &mut hasher);
490 hasher.finish()
491 }
492
493 pub fn render_hash(&self) -> u64 {
495 let mut hasher = FxHasher::default();
496 hash_span_style(&self.span_style, &mut hasher);
497 hash_paragraph_style(&self.paragraph_style, &mut hasher);
498 hasher.finish()
499 }
500}
501
502fn merge_foreground_style(
503 current: &SpanStyle,
504 incoming: &SpanStyle,
505) -> (Option<Color>, Option<Brush>) {
506 if let Some(brush) = incoming.brush.clone() {
507 return (None, Some(brush));
508 }
509 if let Some(color) = incoming.color {
510 return (Some(color), None);
511 }
512 (current.color, current.brush.clone())
513}
514
515fn solid_brush_color(brush: Option<&Brush>) -> Option<Color> {
516 match brush {
517 Some(Brush::Solid(color)) => Some(*color),
518 _ => None,
519 }
520}
521
522fn create_platform_text_style(
523 explicit: Option<PlatformTextStyle>,
524 span_style: Option<PlatformSpanStyle>,
525 paragraph_style: Option<PlatformParagraphStyle>,
526) -> Option<PlatformTextStyle> {
527 let explicit_span = explicit.and_then(|style| style.span_style);
528 let explicit_paragraph = explicit.and_then(|style| style.paragraph_style);
529 let span = span_style.or(explicit_span);
530 let paragraph = paragraph_style.or(explicit_paragraph);
531 if span.is_none() && paragraph.is_none() {
532 None
533 } else {
534 Some(PlatformTextStyle {
535 span_style: span,
536 paragraph_style: paragraph,
537 })
538 }
539}
540
541fn merge_text_unit(current: TextUnit, incoming: TextUnit) -> TextUnit {
542 if matches!(incoming, TextUnit::Unspecified) {
543 current
544 } else {
545 incoming
546 }
547}
548
549fn merge_text_align(current: TextAlign, incoming: TextAlign) -> TextAlign {
550 if matches!(incoming, TextAlign::Unspecified) {
551 current
552 } else {
553 incoming
554 }
555}
556
557fn merge_text_direction(current: TextDirection, incoming: TextDirection) -> TextDirection {
558 if matches!(incoming, TextDirection::Unspecified) {
559 current
560 } else {
561 incoming
562 }
563}
564
565fn merge_line_break(current: LineBreak, incoming: LineBreak) -> LineBreak {
566 if matches!(incoming, LineBreak::Unspecified) {
567 current
568 } else {
569 incoming
570 }
571}
572
573fn merge_hyphens(current: Hyphens, incoming: Hyphens) -> Hyphens {
574 if matches!(incoming, Hyphens::Unspecified) {
575 current
576 } else {
577 incoming
578 }
579}
580
581pub(crate) fn hash_span_measurement<H: Hasher>(span: &SpanStyle, state: &mut H) {
584 hash_text_unit(span.font_size, state);
585 span.font_weight.hash(state);
586 span.font_style.hash(state);
587 span.font_synthesis.hash(state);
588 span.font_family.hash(state);
589 span.font_feature_settings.hash(state);
590 hash_text_unit(span.letter_spacing, state);
591 hash_option_baseline_shift(&span.baseline_shift, state);
592 hash_option_geometric_transform(&span.text_geometric_transform, state);
593 span.locale_list.hash(state);
594 span.platform_style.hash(state);
595}
596
597fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
598 value.to_bits().hash(state);
599}
600
601fn hash_option_color<H: Hasher>(color: &Option<Color>, state: &mut H) {
602 match color {
603 Some(color) => {
604 1u8.hash(state);
605 color.render_hash().hash(state);
606 }
607 None => 0u8.hash(state),
608 }
609}
610
611fn hash_option_brush<H: Hasher>(brush: &Option<Brush>, state: &mut H) {
612 match brush {
613 Some(brush) => {
614 1u8.hash(state);
615 brush.render_hash().hash(state);
616 }
617 None => 0u8.hash(state),
618 }
619}
620
621fn hash_option_alpha<H: Hasher>(alpha: &Option<f32>, state: &mut H) {
622 match alpha {
623 Some(alpha) => {
624 1u8.hash(state);
625 hash_f32_bits(*alpha, state);
626 }
627 None => 0u8.hash(state),
628 }
629}
630
631fn hash_text_unit<H: Hasher>(unit: TextUnit, state: &mut H) {
632 match unit {
633 TextUnit::Unspecified => 0u8.hash(state),
634 TextUnit::Sp(value) => {
635 1u8.hash(state);
636 hash_f32_bits(value, state);
637 }
638 TextUnit::Em(value) => {
639 2u8.hash(state);
640 hash_f32_bits(value, state);
641 }
642 }
643}
644
645fn hash_option_baseline_shift<H: Hasher>(shift: &Option<BaselineShift>, state: &mut H) {
646 match shift {
647 Some(shift) => {
648 1u8.hash(state);
649 hash_f32_bits(shift.0, state);
650 }
651 None => 0u8.hash(state),
652 }
653}
654
655fn hash_option_geometric_transform<H: Hasher>(
656 transform: &Option<TextGeometricTransform>,
657 state: &mut H,
658) {
659 match transform {
660 Some(transform) => {
661 1u8.hash(state);
662 hash_f32_bits(transform.scale_x, state);
663 hash_f32_bits(transform.skew_x, state);
664 }
665 None => 0u8.hash(state),
666 }
667}
668
669fn hash_option_text_indent<H: Hasher>(indent: &Option<TextIndent>, state: &mut H) {
670 match indent {
671 Some(indent) => {
672 1u8.hash(state);
673 hash_text_unit(indent.first_line, state);
674 hash_text_unit(indent.rest_line, state);
675 }
676 None => 0u8.hash(state),
677 }
678}
679
680fn hash_option_shadow<H: Hasher>(shadow: &Option<Shadow>, state: &mut H) {
681 match shadow {
682 Some(shadow) => {
683 1u8.hash(state);
684 shadow.color.render_hash().hash(state);
685 hash_f32_bits(shadow.offset.x, state);
686 hash_f32_bits(shadow.offset.y, state);
687 hash_f32_bits(shadow.blur_radius, state);
688 }
689 None => 0u8.hash(state),
690 }
691}
692
693fn hash_option_text_draw_style<H: Hasher>(draw_style: &Option<TextDrawStyle>, state: &mut H) {
694 match draw_style {
695 Some(TextDrawStyle::Fill) => {
696 1u8.hash(state);
697 0u8.hash(state);
698 }
699 Some(TextDrawStyle::Stroke { width }) => {
700 1u8.hash(state);
701 1u8.hash(state);
702 hash_f32_bits(*width, state);
703 }
704 None => 0u8.hash(state),
705 }
706}
707
708fn hash_span_style<H: Hasher>(span: &SpanStyle, state: &mut H) {
709 hash_span_measurement(span, state);
710 hash_option_color(&span.color, state);
711 hash_option_brush(&span.brush, state);
712 hash_option_alpha(&span.alpha, state);
713 hash_option_color(&span.background, state);
714 span.text_decoration.hash(state);
715 hash_option_shadow(&span.shadow, state);
716 hash_option_text_draw_style(&span.draw_style, state);
717}
718
719fn hash_paragraph_style<H: Hasher>(paragraph: &ParagraphStyle, state: &mut H) {
720 paragraph.text_align.hash(state);
721 paragraph.text_direction.hash(state);
722 hash_text_unit(paragraph.line_height, state);
723 hash_option_text_indent(¶graph.text_indent, state);
724 paragraph.platform_style.hash(state);
725 paragraph.line_height_style.hash(state);
726 paragraph.line_break.hash(state);
727 paragraph.hyphens.hash(state);
728 paragraph.text_motion.hash(state);
729}
730
731#[cfg(test)]
732#[path = "tests/style_tests.rs"]
733mod tests;