Skip to main content

cranpose_ui/text/
style.rs

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