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)]
22pub struct BaselineShift(pub f32);
23
24impl BaselineShift {
25 pub const SUPERSCRIPT: Self = Self(0.5);
26 pub const SUBSCRIPT: Self = Self(-0.5);
27 pub const NONE: Self = Self(0.0);
28 pub const UNSPECIFIED: Self = Self(f32::NAN);
29
30 pub fn is_specified(self) -> bool {
31 !self.0.is_nan()
32 }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct TextGeometricTransform {
37 pub scale_x: f32,
38 pub skew_x: f32,
39}
40
41impl Default for TextGeometricTransform {
42 fn default() -> Self {
43 Self {
44 scale_x: 1.0,
45 skew_x: 0.0,
46 }
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
51pub struct LocaleList {
52 locales: std::sync::Arc<[String]>,
53}
54
55impl LocaleList {
56 pub fn new(locales: Vec<String>) -> Self {
57 Self {
58 locales: locales.into(),
59 }
60 }
61
62 pub fn from_language_tags(tags: &str) -> Self {
63 let locales: Vec<_> = tags
64 .split(',')
65 .map(str::trim)
66 .filter(|tag| !tag.is_empty())
67 .map(ToString::to_string)
68 .collect();
69 Self::new(locales)
70 }
71
72 pub fn locales(&self) -> &[String] {
73 &self.locales
74 }
75
76 pub fn is_empty(&self) -> bool {
77 self.locales.is_empty()
78 }
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
82pub enum TextMotion {
83 #[default]
84 Static,
85 Animated,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
89pub struct PlatformSpanStyle;
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
92pub enum TextShaping {
93 Basic,
94 Advanced,
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
98pub struct PlatformParagraphStyle {
99 pub include_font_padding: Option<bool>,
100 pub shaping: Option<TextShaping>,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
104pub struct PlatformTextStyle {
105 pub span_style: Option<PlatformSpanStyle>,
106 pub paragraph_style: Option<PlatformParagraphStyle>,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Default)]
110pub enum TextDrawStyle {
111 #[default]
112 Fill,
113 Stroke {
114 width: f32,
115 },
116}
117
118#[derive(Clone, Debug, PartialEq)]
119pub struct SpanStyle {
120 pub color: Option<Color>,
121 pub brush: Option<Brush>,
122 pub alpha: Option<f32>,
123 pub font_size: TextUnit,
124 pub font_weight: Option<FontWeight>,
125 pub font_style: Option<FontStyle>,
126 pub font_synthesis: Option<FontSynthesis>,
127 pub font_family: Option<FontFamily>,
128 pub font_feature_settings: Option<std::sync::Arc<str>>,
129 pub letter_spacing: TextUnit,
130 pub baseline_shift: Option<BaselineShift>,
131 pub text_geometric_transform: Option<TextGeometricTransform>,
132 pub locale_list: Option<LocaleList>,
133 pub background: Option<Color>,
134 pub text_decoration: Option<TextDecoration>,
135 pub shadow: Option<Shadow>,
136 pub platform_style: Option<PlatformSpanStyle>,
137 pub draw_style: Option<TextDrawStyle>,
138}
139
140impl Default for SpanStyle {
141 fn default() -> Self {
142 Self {
143 color: None,
144 brush: None,
145 alpha: None,
146 font_size: TextUnit::Unspecified,
147 font_weight: None,
148 font_style: None,
149 font_synthesis: None,
150 font_family: None,
151 font_feature_settings: None,
152 letter_spacing: TextUnit::Unspecified,
153 baseline_shift: None,
154 text_geometric_transform: None,
155 locale_list: None,
156 background: None,
157 text_decoration: None,
158 shadow: None,
159 platform_style: None,
160 draw_style: None,
161 }
162 }
163}
164
165impl SpanStyle {
166 pub fn merge(&self, other: &SpanStyle) -> SpanStyle {
167 let (merged_color, merged_brush) = merge_foreground_style(self, other);
168 SpanStyle {
169 color: merged_color,
170 brush: merged_brush,
171 alpha: other.alpha.or(self.alpha),
172 font_size: merge_text_unit(self.font_size, other.font_size),
173 font_weight: other.font_weight.or(self.font_weight),
174 font_style: other.font_style.or(self.font_style),
175 font_synthesis: other.font_synthesis.or(self.font_synthesis),
176 font_family: other
177 .font_family
178 .clone()
179 .or_else(|| self.font_family.clone()),
180 font_feature_settings: other
181 .font_feature_settings
182 .clone()
183 .or_else(|| self.font_feature_settings.clone()),
184 letter_spacing: merge_text_unit(self.letter_spacing, other.letter_spacing),
185 baseline_shift: other.baseline_shift.or(self.baseline_shift),
186 text_geometric_transform: other
187 .text_geometric_transform
188 .or(self.text_geometric_transform),
189 locale_list: other
190 .locale_list
191 .clone()
192 .or_else(|| self.locale_list.clone()),
193 background: other.background.or(self.background),
194 text_decoration: other.text_decoration.or(self.text_decoration),
195 shadow: other.shadow.or(self.shadow),
196 platform_style: other.platform_style.or(self.platform_style),
197 draw_style: other.draw_style.or(self.draw_style),
198 }
199 }
200
201 pub fn plus(&self, other: &SpanStyle) -> SpanStyle {
202 self.merge(other)
203 }
204
205 pub fn resolve_font_size(&self, default_size: f32) -> f32 {
206 let fallback = if default_size.is_finite() && default_size > 0.0 {
207 default_size
208 } else {
209 14.0
210 };
211 match self.font_size {
212 TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
213 TextUnit::Em(value) if value.is_finite() && value > 0.0 => value * fallback,
214 _ => fallback,
215 }
216 }
217
218 pub fn resolve_foreground_color(&self, default_color: Color) -> Color {
219 let mut color = self
220 .color
221 .or_else(|| solid_brush_color(self.brush.as_ref()))
222 .unwrap_or(default_color);
223 if let Some(alpha) = self.alpha {
224 color.3 *= alpha.clamp(0.0, 1.0);
225 }
226 color
227 }
228
229 pub fn render_hash(&self) -> u64 {
230 let mut hasher = FxHasher::default();
231 hash_span_style(self, &mut hasher);
232 hasher.finish()
233 }
234}
235
236#[derive(Clone, Debug, PartialEq)]
237pub struct ParagraphStyle {
238 pub text_align: TextAlign,
239 pub text_direction: TextDirection,
240 pub line_height: TextUnit,
241 pub text_indent: Option<TextIndent>,
242 pub platform_style: Option<PlatformParagraphStyle>,
243 pub line_height_style: Option<LineHeightStyle>,
244 pub line_break: LineBreak,
245 pub hyphens: Hyphens,
246 pub text_motion: Option<TextMotion>,
247}
248
249impl Default for ParagraphStyle {
250 fn default() -> Self {
251 Self {
252 text_align: TextAlign::Unspecified,
253 text_direction: TextDirection::Unspecified,
254 line_height: TextUnit::Unspecified,
255 text_indent: None,
256 platform_style: None,
257 line_height_style: None,
258 line_break: LineBreak::Unspecified,
259 hyphens: Hyphens::Unspecified,
260 text_motion: None,
261 }
262 }
263}
264
265impl ParagraphStyle {
266 pub fn merge(&self, other: &ParagraphStyle) -> ParagraphStyle {
267 ParagraphStyle {
268 text_align: merge_text_align(self.text_align, other.text_align),
269 text_direction: merge_text_direction(self.text_direction, other.text_direction),
270 line_height: merge_text_unit(self.line_height, other.line_height),
271 text_indent: other.text_indent.or(self.text_indent),
272 platform_style: other.platform_style.or(self.platform_style),
273 line_height_style: other.line_height_style.or(self.line_height_style),
274 line_break: merge_line_break(self.line_break, other.line_break),
275 hyphens: merge_hyphens(self.hyphens, other.hyphens),
276 text_motion: other.text_motion.or(self.text_motion),
277 }
278 }
279
280 pub fn plus(&self, other: &ParagraphStyle) -> ParagraphStyle {
281 self.merge(other)
282 }
283
284 pub fn render_hash(&self) -> u64 {
285 let mut hasher = FxHasher::default();
286 hash_paragraph_style(self, &mut hasher);
287 hasher.finish()
288 }
289}
290
291#[derive(Clone, Debug, PartialEq, Default)]
292pub struct TextStyle {
293 pub span_style: SpanStyle,
294 pub paragraph_style: ParagraphStyle,
295}
296
297impl TextStyle {
298 pub fn new(span_style: SpanStyle, paragraph_style: ParagraphStyle) -> Self {
299 Self {
300 span_style,
301 paragraph_style,
302 }
303 }
304
305 pub fn from_span_style(span_style: SpanStyle) -> Self {
306 Self::new(span_style, ParagraphStyle::default())
307 }
308
309 pub fn from_paragraph_style(paragraph_style: ParagraphStyle) -> Self {
310 Self::new(SpanStyle::default(), paragraph_style)
311 }
312
313 pub fn merge(&self, other: &TextStyle) -> TextStyle {
314 TextStyle {
315 span_style: self.span_style.merge(&other.span_style),
316 paragraph_style: self.paragraph_style.merge(&other.paragraph_style),
317 }
318 }
319
320 pub fn plus(&self, other: &TextStyle) -> TextStyle {
321 self.merge(other)
322 }
323
324 pub fn to_span_style(&self) -> SpanStyle {
325 self.span_style.clone()
326 }
327
328 pub fn to_paragraph_style(&self) -> ParagraphStyle {
329 self.paragraph_style.clone()
330 }
331
332 pub fn platform_style(&self) -> Option<PlatformTextStyle> {
333 create_platform_text_style(
334 None,
335 self.span_style.platform_style,
336 self.paragraph_style.platform_style,
337 )
338 }
339
340 pub fn with_platform_style(mut self, platform_style: Option<PlatformTextStyle>) -> Self {
341 self.span_style.platform_style = platform_style.and_then(|style| style.span_style);
342 self.paragraph_style.platform_style =
343 platform_style.and_then(|style| style.paragraph_style);
344 self
345 }
346
347 pub fn resolve_font_size(&self, default_size: f32) -> f32 {
348 self.span_style.resolve_font_size(default_size)
349 }
350
351 pub fn resolve_line_height(&self, default_size: f32, natural_line_height: f32) -> f32 {
352 let fallback = if natural_line_height.is_finite() && natural_line_height > 0.0 {
353 natural_line_height
354 } else {
355 self.resolve_font_size(default_size)
356 };
357 match self.paragraph_style.line_height {
358 TextUnit::Sp(value) if value.is_finite() && value > 0.0 => value,
359 TextUnit::Em(value) if value.is_finite() && value > 0.0 => {
360 value * self.resolve_font_size(default_size)
361 }
362 _ => fallback,
363 }
364 }
365
366 pub fn resolve_letter_spacing(&self, default_size: f32) -> f32 {
367 let font_size = self.resolve_font_size(default_size);
368 match self.span_style.letter_spacing {
369 TextUnit::Sp(value) if value.is_finite() => value,
370 TextUnit::Em(value) if value.is_finite() => value * font_size,
371 _ => 0.0,
372 }
373 }
374
375 pub fn resolve_text_color(&self, default_color: Color) -> Color {
376 self.span_style.resolve_foreground_color(default_color)
377 }
378
379 pub fn measurement_hash(&self) -> u64 {
380 let mut hasher = FxHasher::default();
381 hash_span_measurement(&self.span_style, &mut hasher);
382 hash_paragraph_style(&self.paragraph_style, &mut hasher);
383 hasher.finish()
384 }
385
386 pub fn render_hash(&self) -> u64 {
387 let mut hasher = FxHasher::default();
388 hash_span_style(&self.span_style, &mut hasher);
389 hash_paragraph_style(&self.paragraph_style, &mut hasher);
390 hasher.finish()
391 }
392}
393
394fn merge_foreground_style(
395 current: &SpanStyle,
396 incoming: &SpanStyle,
397) -> (Option<Color>, Option<Brush>) {
398 if let Some(brush) = incoming.brush.clone() {
399 return (None, Some(brush));
400 }
401 if let Some(color) = incoming.color {
402 return (Some(color), None);
403 }
404 (current.color, current.brush.clone())
405}
406
407fn solid_brush_color(brush: Option<&Brush>) -> Option<Color> {
408 match brush {
409 Some(Brush::Solid(color)) => Some(*color),
410 _ => None,
411 }
412}
413
414fn create_platform_text_style(
415 explicit: Option<PlatformTextStyle>,
416 span_style: Option<PlatformSpanStyle>,
417 paragraph_style: Option<PlatformParagraphStyle>,
418) -> Option<PlatformTextStyle> {
419 let explicit_span = explicit.and_then(|style| style.span_style);
420 let explicit_paragraph = explicit.and_then(|style| style.paragraph_style);
421 let span = span_style.or(explicit_span);
422 let paragraph = paragraph_style.or(explicit_paragraph);
423 if span.is_none() && paragraph.is_none() {
424 None
425 } else {
426 Some(PlatformTextStyle {
427 span_style: span,
428 paragraph_style: paragraph,
429 })
430 }
431}
432
433fn merge_text_unit(current: TextUnit, incoming: TextUnit) -> TextUnit {
434 if matches!(incoming, TextUnit::Unspecified) {
435 current
436 } else {
437 incoming
438 }
439}
440
441fn merge_text_align(current: TextAlign, incoming: TextAlign) -> TextAlign {
442 if matches!(incoming, TextAlign::Unspecified) {
443 current
444 } else {
445 incoming
446 }
447}
448
449fn merge_text_direction(current: TextDirection, incoming: TextDirection) -> TextDirection {
450 if matches!(incoming, TextDirection::Unspecified) {
451 current
452 } else {
453 incoming
454 }
455}
456
457fn merge_line_break(current: LineBreak, incoming: LineBreak) -> LineBreak {
458 if matches!(incoming, LineBreak::Unspecified) {
459 current
460 } else {
461 incoming
462 }
463}
464
465fn merge_hyphens(current: Hyphens, incoming: Hyphens) -> Hyphens {
466 if matches!(incoming, Hyphens::Unspecified) {
467 current
468 } else {
469 incoming
470 }
471}
472
473pub(crate) fn hash_span_measurement<H: Hasher>(span: &SpanStyle, state: &mut H) {
476 hash_text_unit(span.font_size, state);
477 span.font_weight.hash(state);
478 span.font_style.hash(state);
479 span.font_synthesis.hash(state);
480 span.font_family.hash(state);
481 span.font_feature_settings.hash(state);
482 hash_text_unit(span.letter_spacing, state);
483 hash_option_baseline_shift(&span.baseline_shift, state);
484 hash_option_geometric_transform(&span.text_geometric_transform, state);
485 span.locale_list.hash(state);
486 span.platform_style.hash(state);
487}
488
489fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
490 value.to_bits().hash(state);
491}
492
493fn hash_option_color<H: Hasher>(color: &Option<Color>, state: &mut H) {
494 match color {
495 Some(color) => {
496 1u8.hash(state);
497 color.render_hash().hash(state);
498 }
499 None => 0u8.hash(state),
500 }
501}
502
503fn hash_option_brush<H: Hasher>(brush: &Option<Brush>, state: &mut H) {
504 match brush {
505 Some(brush) => {
506 1u8.hash(state);
507 brush.render_hash().hash(state);
508 }
509 None => 0u8.hash(state),
510 }
511}
512
513fn hash_option_alpha<H: Hasher>(alpha: &Option<f32>, state: &mut H) {
514 match alpha {
515 Some(alpha) => {
516 1u8.hash(state);
517 hash_f32_bits(*alpha, state);
518 }
519 None => 0u8.hash(state),
520 }
521}
522
523fn hash_text_unit<H: Hasher>(unit: TextUnit, state: &mut H) {
524 match unit {
525 TextUnit::Unspecified => 0u8.hash(state),
526 TextUnit::Sp(value) => {
527 1u8.hash(state);
528 hash_f32_bits(value, state);
529 }
530 TextUnit::Em(value) => {
531 2u8.hash(state);
532 hash_f32_bits(value, state);
533 }
534 }
535}
536
537fn hash_option_baseline_shift<H: Hasher>(shift: &Option<BaselineShift>, state: &mut H) {
538 match shift {
539 Some(shift) => {
540 1u8.hash(state);
541 hash_f32_bits(shift.0, state);
542 }
543 None => 0u8.hash(state),
544 }
545}
546
547fn hash_option_geometric_transform<H: Hasher>(
548 transform: &Option<TextGeometricTransform>,
549 state: &mut H,
550) {
551 match transform {
552 Some(transform) => {
553 1u8.hash(state);
554 hash_f32_bits(transform.scale_x, state);
555 hash_f32_bits(transform.skew_x, state);
556 }
557 None => 0u8.hash(state),
558 }
559}
560
561fn hash_option_text_indent<H: Hasher>(indent: &Option<TextIndent>, state: &mut H) {
562 match indent {
563 Some(indent) => {
564 1u8.hash(state);
565 hash_text_unit(indent.first_line, state);
566 hash_text_unit(indent.rest_line, state);
567 }
568 None => 0u8.hash(state),
569 }
570}
571
572fn hash_option_shadow<H: Hasher>(shadow: &Option<Shadow>, state: &mut H) {
573 match shadow {
574 Some(shadow) => {
575 1u8.hash(state);
576 shadow.color.render_hash().hash(state);
577 hash_f32_bits(shadow.offset.x, state);
578 hash_f32_bits(shadow.offset.y, state);
579 hash_f32_bits(shadow.blur_radius, state);
580 }
581 None => 0u8.hash(state),
582 }
583}
584
585fn hash_option_text_draw_style<H: Hasher>(draw_style: &Option<TextDrawStyle>, state: &mut H) {
586 match draw_style {
587 Some(TextDrawStyle::Fill) => {
588 1u8.hash(state);
589 0u8.hash(state);
590 }
591 Some(TextDrawStyle::Stroke { width }) => {
592 1u8.hash(state);
593 1u8.hash(state);
594 hash_f32_bits(*width, state);
595 }
596 None => 0u8.hash(state),
597 }
598}
599
600fn hash_span_style<H: Hasher>(span: &SpanStyle, state: &mut H) {
601 hash_span_measurement(span, state);
602 hash_option_color(&span.color, state);
603 hash_option_brush(&span.brush, state);
604 hash_option_alpha(&span.alpha, state);
605 hash_option_color(&span.background, state);
606 span.text_decoration.hash(state);
607 hash_option_shadow(&span.shadow, state);
608 hash_option_text_draw_style(&span.draw_style, state);
609}
610
611fn hash_paragraph_style<H: Hasher>(paragraph: &ParagraphStyle, state: &mut H) {
612 paragraph.text_align.hash(state);
613 paragraph.text_direction.hash(state);
614 hash_text_unit(paragraph.line_height, state);
615 hash_option_text_indent(¶graph.text_indent, state);
616 paragraph.platform_style.hash(state);
617 paragraph.line_height_style.hash(state);
618 paragraph.line_break.hash(state);
619 paragraph.hyphens.hash(state);
620 paragraph.text_motion.hash(state);
621}
622
623pub fn text_align_fraction(text_style: &TextStyle, text: &str) -> f32 {
637 let paragraph_style = &text_style.paragraph_style;
638 match paragraph_style.text_align {
639 super::TextAlign::Left => 0.0,
640 super::TextAlign::Right => 1.0,
641 super::TextAlign::Center => 0.5,
642 super::TextAlign::Start
643 | super::TextAlign::End
644 | super::TextAlign::Justify
645 | super::TextAlign::Unspecified => {
646 let direction =
647 super::resolve_text_direction(text, Some(paragraph_style.text_direction));
648 let start = match direction {
649 super::ResolvedTextDirection::Ltr => 0.0,
650 super::ResolvedTextDirection::Rtl => 1.0,
651 };
652 if paragraph_style.text_align == super::TextAlign::End {
653 1.0 - start
654 } else {
655 start
656 }
657 }
658 }
659}
660
661#[cfg(test)]
662#[path = "tests/style_tests.rs"]
663mod tests;