Skip to main content

cranpose_text/
style.rs

1use std::hash::{Hash, Hasher};
2
3use cranpose_ui_graphics::{Brush, Color, FxHasher, RenderHash};
4/// The line-height policy lives with the drawing-side text types, because a
5/// [`DrawScope`](cranpose_ui_graphics::DrawScope) has to be able to name it too:
6/// a run drawn through a canvas and a `Text` composable on the same screen must
7/// resolve their line boxes by the same rule, and the only style a draw scope
8/// can carry is the flat one from `cranpose-ui-graphics`.
9pub 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/// Vertical shift of text from its baseline as a fraction of the font size. Positive values
21/// move the text up. Compose's `BaselineShift`.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct BaselineShift(pub f32);
24
25impl BaselineShift {
26    /// Moves the text up by half the font size.
27    pub const SUPERSCRIPT: Self = Self(0.5);
28    /// Moves the text down by half the font size.
29    pub const SUBSCRIPT: Self = Self(-0.5);
30    /// No shift.
31    pub const NONE: Self = Self(0.0);
32    /// No value is set and the shift is inherited. Holds `NaN`.
33    pub const UNSPECIFIED: Self = Self(f32::NAN);
34
35    /// Returns false for [`BaselineShift::UNSPECIFIED`].
36    pub fn is_specified(self) -> bool {
37        !self.0.is_nan()
38    }
39}
40
41/// Horizontal scaling and skew of glyphs. Compose's `TextGeometricTransform`. Text rendering
42/// does not apply it yet.
43#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct TextGeometricTransform {
45    /// Horizontal scale factor of the glyphs. 1.0 leaves them unscaled.
46    pub scale_x: f32,
47    /// Horizontal skew of the glyphs. 0.0 leaves them upright.
48    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/// Ordered list of language tags, most preferred first. They select language-specific glyph
61/// forms and hyphenation rules. Compose's `LocaleList`.
62#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
63pub struct LocaleList {
64    locales: std::sync::Arc<[String]>,
65}
66
67impl LocaleList {
68    /// Creates a list from `locales`, which are language tags such as `en-US`.
69    pub fn new(locales: Vec<String>) -> Self {
70        Self {
71            locales: locales.into(),
72        }
73    }
74
75    /// Parses a comma-separated list of language tags such as `en-US, fr`. Blank entries are
76    /// skipped.
77    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    /// The language tags, most preferred first.
88    pub fn locales(&self) -> &[String] {
89        &self.locales
90    }
91
92    /// Returns true when the list holds no tags.
93    pub fn is_empty(&self) -> bool {
94        self.locales.is_empty()
95    }
96}
97
98/// Whether text is expected to move, scale or rotate. Compose's `TextMotion`.
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
100pub enum TextMotion {
101    /// The text stays in place. Glyphs are placed on whole pixels for sharp edges.
102    #[default]
103    Static,
104    /// The text may move, scale or rotate. Glyphs keep sub-pixel positions so motion stays
105    /// smooth.
106    Animated,
107}
108
109/// Platform-specific settings of a span. It holds no options yet.
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
111pub struct PlatformSpanStyle;
112
113/// How much work the text shaper does for a paragraph.
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115pub enum TextShaping {
116    /// Text is shaped only when it has non-ASCII characters. ASCII text uses plain glyph advances.
117    Basic,
118    /// The full OpenType shaper always runs, including on ASCII text.
119    Advanced,
120}
121
122/// Platform-specific settings of a paragraph. Compose's `PlatformParagraphStyle`.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
124pub struct PlatformParagraphStyle {
125    /// When `Some(true)`, the font's line gap is added to the line height, as with Android's
126    /// `includeFontPadding`. `None` and `Some(false)` add nothing.
127    pub include_font_padding: Option<bool>,
128    /// Shaping mode. `None` acts like [`TextShaping::Basic`].
129    pub shaping: Option<TextShaping>,
130}
131
132/// Platform-specific settings of a span and of a paragraph. Compose's `PlatformTextStyle`.
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
134pub struct PlatformTextStyle {
135    /// Settings for spans.
136    pub span_style: Option<PlatformSpanStyle>,
137    /// Settings for paragraphs.
138    pub paragraph_style: Option<PlatformParagraphStyle>,
139}
140
141/// Whether glyphs are filled or only outlined.
142#[derive(Clone, Copy, Debug, PartialEq, Default)]
143pub enum TextDrawStyle {
144    /// Glyphs are filled with the text color or brush.
145    #[default]
146    Fill,
147    /// Only the outlines of the glyphs are drawn.
148    Stroke {
149        /// Outline width in logical pixels. Zero, negative and non-finite widths draw a fill.
150        width: f32,
151    },
152}
153
154/// Style of a span of characters: color, font, spacing and decoration. Compose's `SpanStyle`.
155/// A field left unset (`None` or `Unspecified`) keeps the inherited value when styles merge.
156#[derive(Clone, Debug, PartialEq)]
157pub struct SpanStyle {
158    /// Text color.
159    pub color: Option<Color>,
160    /// Brush that paints the glyphs, for example a gradient. Merging a style that sets `color`
161    /// clears an inherited `brush`, and the reverse.
162    pub brush: Option<Brush>,
163    /// Opacity from 0.0 to 1.0, multiplied into the color or brush. `None` leaves it unchanged.
164    pub alpha: Option<f32>,
165    /// Font size in `Sp` or as a multiple of the default size with `Em`.
166    pub font_size: TextUnit,
167    /// Weight of the font.
168    pub font_weight: Option<FontWeight>,
169    /// Upright or italic glyphs.
170    pub font_style: Option<FontStyle>,
171    /// Which missing bold or italic faces may be faked. `None` allows both.
172    pub font_synthesis: Option<FontSynthesis>,
173    /// Font family.
174    pub font_family: Option<FontFamily>,
175    /// OpenType feature settings in CSS `font-feature-settings` syntax, such as `smcp, liga 0`.
176    pub font_feature_settings: Option<crate::font_feature::FontFeatureSettings>,
177    /// Extra space added after each character, in `Sp` or as a multiple of the font size.
178    pub letter_spacing: TextUnit,
179    /// Vertical shift of the glyphs from the baseline.
180    pub baseline_shift: Option<BaselineShift>,
181    /// Horizontal scale and skew of the glyphs. Text rendering does not apply it yet.
182    pub text_geometric_transform: Option<TextGeometricTransform>,
183    /// Languages of the text, which select language-specific glyph forms and hyphenation rules.
184    pub locale_list: Option<LocaleList>,
185    /// Color drawn behind the glyphs.
186    pub background: Option<Color>,
187    /// Underline and line through.
188    pub text_decoration: Option<TextDecoration>,
189    /// Shadow drawn behind the glyphs.
190    pub shadow: Option<Shadow>,
191    /// Platform-specific settings.
192    pub platform_style: Option<PlatformSpanStyle>,
193    /// Fill or stroke. `None` fills the glyphs.
194    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    /// Returns a style where each field set in `other` replaces the one in `self`. Fields unset
224    /// in `other` keep the value of `self`.
225    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    /// Same as [`SpanStyle::merge`]. Compose's `SpanStyle.plus`.
261    pub fn plus(&self, other: &Self) -> Self {
262        self.merge(other)
263    }
264
265    /// Returns the font size in sp. An `Sp` size is used as is, an `Em` size multiplies
266    /// `default_size`, and an unspecified, non-positive or non-finite size gives `default_size`.
267    /// A `default_size` that is not positive and finite counts as 14.0.
268    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    /// Returns `color`, else the color of a solid `brush`, else `default_color`, with `alpha`
282    /// multiplied into the result's alpha channel.
283    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    /// Hash of every field that changes how the text looks, as renderers use it to key caches.
295    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/// Style of a whole paragraph: alignment, direction, line height and line breaking.
303/// Compose's `ParagraphStyle`.
304#[derive(Clone, Debug, PartialEq)]
305pub struct ParagraphStyle {
306    /// Horizontal alignment of the lines.
307    pub text_align: TextAlign,
308    /// Base direction of the paragraph.
309    pub text_direction: TextDirection,
310    /// Distance between baselines, in `Sp` or as a multiple of the font size. `Unspecified` uses
311    /// the natural line height of the font.
312    pub line_height: TextUnit,
313    /// Indent of the first line and of the other lines. Text layout does not apply it yet.
314    pub text_indent: Option<TextIndent>,
315    /// Platform-specific settings.
316    pub platform_style: Option<PlatformParagraphStyle>,
317    /// How the line height is placed in each line and trimmed at the edges of the paragraph.
318    pub line_height_style: Option<LineHeightStyle>,
319    /// Strategy that breaks the paragraph into lines.
320    pub line_break: LineBreak,
321    /// Whether words may be split with a hyphen.
322    pub hyphens: Hyphens,
323    /// Whether the text moves. `None` counts as [`TextMotion::Static`].
324    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    /// Returns a style where each field set in `other` replaces the one in `self`. Fields unset
345    /// in `other` keep the value of `self`.
346    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    /// Same as [`ParagraphStyle::merge`]. Compose's `ParagraphStyle.plus`.
361    pub fn plus(&self, other: &Self) -> Self {
362        self.merge(other)
363    }
364
365    /// Hash of every field that changes how the paragraph looks, as renderers use it to key
366    /// caches.
367    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/// Complete style of a text: a [`SpanStyle`] and a [`ParagraphStyle`]. Compose's `TextStyle`.
375#[derive(Clone, Debug, PartialEq, Default)]
376pub struct TextStyle {
377    /// Settings that apply to characters.
378    pub span_style: SpanStyle,
379    /// Settings that apply to the whole paragraph.
380    pub paragraph_style: ParagraphStyle,
381}
382
383impl TextStyle {
384    /// Creates a style from `span_style` and `paragraph_style`.
385    pub fn new(span_style: SpanStyle, paragraph_style: ParagraphStyle) -> Self {
386        Self {
387            span_style,
388            paragraph_style,
389        }
390    }
391
392    /// Creates a style with `span_style` and default paragraph settings.
393    pub fn from_span_style(span_style: SpanStyle) -> Self {
394        Self::new(span_style, ParagraphStyle::default())
395    }
396
397    /// Creates a style with `paragraph_style` and default span settings.
398    pub fn from_paragraph_style(paragraph_style: ParagraphStyle) -> Self {
399        Self::new(SpanStyle::default(), paragraph_style)
400    }
401
402    /// Returns a style that merges the span and paragraph styles of `other` over those of
403    /// `self`.
404    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    /// Same as [`TextStyle::merge`]. Compose's `TextStyle.plus`.
412    pub fn plus(&self, other: &Self) -> Self {
413        self.merge(other)
414    }
415
416    /// Returns a copy of the span style.
417    pub fn to_span_style(&self) -> SpanStyle {
418        self.span_style.clone()
419    }
420
421    /// Returns a copy of the paragraph style.
422    pub fn to_paragraph_style(&self) -> ParagraphStyle {
423        self.paragraph_style.clone()
424    }
425
426    /// Returns the platform-specific settings of the span and the paragraph, or `None` when
427    /// neither has any.
428    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    /// Returns this style with the platform-specific settings of the span and the paragraph
437    /// replaced by those in `platform_style`.
438    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    /// Returns the font size in sp. See [`SpanStyle::resolve_font_size`].
446    pub fn resolve_font_size(&self, default_size: f32) -> f32 {
447        self.span_style.resolve_font_size(default_size)
448    }
449
450    /// Returns the line height in the unit of `default_size`. A positive `Sp` line height is used
451    /// as is and a positive `Em` one multiplies the font size. Otherwise the result is
452    /// `natural_line_height`, or the font size when that is not positive and finite.
453    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    /// Returns the letter spacing in the unit of `default_size`. An `Em` value multiplies the
469    /// font size. An unspecified or non-finite spacing gives 0.0.
470    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    /// Returns the text color. See [`SpanStyle::resolve_foreground_color`].
480    pub fn resolve_text_color(&self, default_color: Color) -> Color {
481        self.span_style.resolve_foreground_color(default_color)
482    }
483
484    /// Hash of the settings that change how the text measures. Colors, decorations and shadows
485    /// are left out.
486    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    /// Hash of every setting that changes how the text looks, as renderers use it to key caches.
494    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
581/// The span attributes that change how text measures, without its colors,
582/// brush, background, decorations or shadow.
583pub(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(&paragraph.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;