Skip to main content

incular_text/style/
text_style.rs

1use super::fonts::{FontFamily, FontFeature, FontVariation, FontWeight};
2use super::text::{
3    FontStyle, LineHeight, TextBaseline, TextDecoration, TextDecorationStyle,
4    TextLeadingDistribution, TextOverflow, TextScaler, TextShadow,
5};
6use incular_config::WidgetDefaults;
7use incular_core::{ChangeImpact, Color, Lerp};
8use std::sync::Arc;
9
10/// An immutable, Flutter-style text style description.
11#[derive(Clone, Debug, PartialEq)]
12pub struct TextStyle {
13    pub inherit: bool,
14    pub has_explicit_color: bool,
15    pub has_explicit_size: bool,
16    pub has_explicit_weight: bool,
17    pub has_explicit_style: bool,
18    pub has_explicit_family: bool,
19    pub family: FontFamily,
20    pub fallback_families: Arc<[FontFamily]>,
21    pub size: f32,
22    pub weight: FontWeight,
23    pub style: FontStyle,
24    pub color: Color,
25    pub background_color: Option<Color>,
26    pub line_height: Option<LineHeight>,
27    pub letter_spacing: f32,
28    pub word_spacing: Option<f32>,
29    pub text_baseline: Option<TextBaseline>,
30    pub leading_distribution: Option<TextLeadingDistribution>,
31    pub locale: Option<String>,
32    pub foreground: Option<Color>,
33    pub background: Option<Color>,
34    pub shadows: Arc<[TextShadow]>,
35    pub font_variations: Option<Arc<str>>,
36    pub font_features: Option<Arc<str>>,
37    pub decoration: Option<TextDecoration>,
38    pub decoration_color: Option<Color>,
39    pub decoration_style: Option<TextDecorationStyle>,
40    pub decoration_thickness: Option<f32>,
41    pub debug_label: Option<String>,
42    pub overflow: Option<TextOverflow>,
43}
44
45impl TextStyle {
46    #[must_use]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    #[must_use]
52    pub fn inherit(mut self, inherit: bool) -> Self {
53        self.inherit = inherit;
54        self
55    }
56
57    #[must_use]
58    pub fn font_family(mut self, family: impl Into<String>) -> Self {
59        self.family = FontFamily::named(family);
60        self.has_explicit_family = true;
61        self
62    }
63
64    #[must_use]
65    pub fn family(mut self, family: FontFamily) -> Self {
66        self.family = family;
67        self.has_explicit_family = true;
68        self
69    }
70
71    #[must_use]
72    pub fn fallback_families<I, S>(mut self, families: I) -> Self
73    where
74        I: IntoIterator<Item = S>,
75        S: Into<String>,
76    {
77        self.fallback_families = families.into_iter().map(FontFamily::named).collect();
78        self
79    }
80
81    #[must_use]
82    pub fn fallback_family(mut self, family: FontFamily) -> Self {
83        let mut families = self.fallback_families.to_vec();
84        families.push(family);
85        self.fallback_families = families.into();
86        self
87    }
88
89    #[must_use]
90    pub fn font_size(mut self, size: f32) -> Self {
91        self.size = size.max(0.0);
92        self.has_explicit_size = true;
93        self
94    }
95
96    #[must_use]
97    pub fn size(self, size: f32) -> Self {
98        self.font_size(size)
99    }
100
101    #[must_use]
102    pub fn font_weight(mut self, weight: FontWeight) -> Self {
103        self.weight = weight;
104        self.has_explicit_weight = true;
105        self
106    }
107
108    #[must_use]
109    pub fn bold(self) -> Self {
110        self.font_weight(FontWeight::BOLD)
111    }
112
113    #[must_use]
114    pub fn font_style(mut self, style: FontStyle) -> Self {
115        self.style = style;
116        self.has_explicit_style = true;
117        self
118    }
119
120    #[must_use]
121    pub fn italic(self) -> Self {
122        self.font_style(FontStyle::Italic)
123    }
124
125    #[must_use]
126    pub fn color(mut self, color: Color) -> Self {
127        self.color = color;
128        self.foreground = None;
129        self.has_explicit_color = true;
130        self
131    }
132
133    #[must_use]
134    pub fn background_color(mut self, color: Color) -> Self {
135        self.background_color = Some(color);
136        self.background = None;
137        self
138    }
139
140    #[must_use]
141    pub fn line_height(mut self, line_height: Option<f32>) -> Self {
142        self.line_height = line_height.map(|value| {
143            if value <= 4.0 {
144                LineHeight::Multiplier(value.max(0.0))
145            } else {
146                LineHeight::Absolute(value.max(0.0))
147            }
148        });
149        self
150    }
151
152    #[must_use]
153    pub fn height(self, height: Option<f32>) -> Self {
154        self.line_height(height)
155    }
156
157    #[must_use]
158    pub fn line_height_multiplier(mut self, factor: f32) -> Self {
159        self.line_height = Some(LineHeight::Multiplier(factor.max(0.0)));
160        self
161    }
162
163    #[must_use]
164    pub fn line_height_absolute(mut self, pixels: f32) -> Self {
165        self.line_height = Some(LineHeight::Absolute(pixels.max(0.0)));
166        self
167    }
168
169    #[must_use]
170    pub fn line_height_mode(mut self, mode: LineHeight) -> Self {
171        self.line_height = Some(mode);
172        self
173    }
174
175    #[must_use]
176    pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
177        self.letter_spacing = letter_spacing;
178        self
179    }
180
181    #[must_use]
182    pub fn word_spacing(mut self, word_spacing: Option<f32>) -> Self {
183        self.word_spacing = word_spacing;
184        self
185    }
186
187    #[must_use]
188    pub fn text_baseline(mut self, baseline: TextBaseline) -> Self {
189        self.text_baseline = Some(baseline);
190        self
191    }
192
193    #[must_use]
194    pub fn leading_distribution(mut self, distribution: TextLeadingDistribution) -> Self {
195        self.leading_distribution = Some(distribution);
196        self
197    }
198
199    #[must_use]
200    pub fn locale(mut self, locale: impl Into<String>) -> Self {
201        self.locale = Some(locale.into());
202        self
203    }
204
205    #[must_use]
206    pub fn foreground(mut self, color: Color) -> Self {
207        self.foreground = Some(color);
208        self
209    }
210
211    #[must_use]
212    pub fn background(mut self, color: Color) -> Self {
213        self.background = Some(color);
214        self.background_color = None;
215        self
216    }
217
218    #[must_use]
219    pub fn shadows<I>(mut self, shadows: I) -> Self
220    where
221        I: IntoIterator<Item = TextShadow>,
222    {
223        self.shadows = shadows.into_iter().collect::<Vec<_>>().into();
224        self
225    }
226
227    #[must_use]
228    pub fn shadow(mut self, shadow: TextShadow) -> Self {
229        let mut shadows = self.shadows.to_vec();
230        shadows.push(shadow);
231        self.shadows = shadows.into();
232        self
233    }
234
235    #[must_use]
236    pub fn font_variations(mut self, settings: impl Into<Arc<str>>) -> Self {
237        self.font_variations = Some(settings.into());
238        self
239    }
240
241    #[must_use]
242    pub fn font_variation_settings(mut self, variations: &[FontVariation]) -> Self {
243        let formatted = variations
244            .iter()
245            .map(FontVariation::to_css_setting)
246            .collect::<Vec<_>>()
247            .join(", ");
248        self.font_variations = Some(formatted.into());
249        self
250    }
251
252    #[must_use]
253    pub fn font_features(mut self, settings: impl Into<Arc<str>>) -> Self {
254        self.font_features = Some(settings.into());
255        self
256    }
257
258    #[must_use]
259    pub fn font_feature_settings(mut self, features: &[FontFeature]) -> Self {
260        let formatted = features
261            .iter()
262            .map(FontFeature::to_css_setting)
263            .collect::<Vec<_>>()
264            .join(", ");
265        self.font_features = Some(formatted.into());
266        self
267    }
268
269    #[must_use]
270    pub fn decoration(mut self, decoration: TextDecoration) -> Self {
271        self.decoration = Some(decoration);
272        self
273    }
274
275    #[must_use]
276    pub fn decoration_color(mut self, color: Color) -> Self {
277        self.decoration_color = Some(color);
278        self
279    }
280
281    #[must_use]
282    pub fn decoration_style(mut self, style: TextDecorationStyle) -> Self {
283        self.decoration_style = Some(style);
284        self
285    }
286
287    #[must_use]
288    pub fn decoration_thickness(mut self, thickness: f32) -> Self {
289        self.decoration_thickness = Some(thickness.max(0.0));
290        self
291    }
292
293    #[must_use]
294    pub fn debug_label(mut self, label: impl Into<String>) -> Self {
295        self.debug_label = Some(label.into());
296        self
297    }
298
299    #[must_use]
300    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
301        self.overflow = Some(overflow);
302        self
303    }
304
305    // Immutable derivation with_* methods
306    #[must_use]
307    pub fn with_font_size(&self, size: f32) -> Self {
308        let mut s = self.clone();
309        s.size = size.max(0.0);
310        s
311    }
312
313    #[must_use]
314    pub fn with_font_weight(&self, weight: FontWeight) -> Self {
315        let mut s = self.clone();
316        s.weight = weight;
317        s
318    }
319
320    #[must_use]
321    pub fn with_color(&self, color: Color) -> Self {
322        let mut s = self.clone();
323        s.color = color;
324        s.has_explicit_color = true;
325        s
326    }
327
328    #[must_use]
329    pub fn with_letter_spacing(&self, letter_spacing: f32) -> Self {
330        let mut s = self.clone();
331        s.letter_spacing = letter_spacing;
332        s
333    }
334
335    #[must_use]
336    pub fn with_line_height(&self, line_height: Option<f32>) -> Self {
337        let mut s = self.clone();
338        s.line_height = line_height.map(|v| {
339            if v <= 4.0 {
340                LineHeight::Multiplier(v.max(0.0))
341            } else {
342                LineHeight::Absolute(v.max(0.0))
343            }
344        });
345        s
346    }
347
348    #[must_use]
349    pub fn with_decoration(&self, decoration: Option<TextDecoration>) -> Self {
350        let mut s = self.clone();
351        s.decoration = decoration;
352        s
353    }
354
355    #[must_use]
356    pub fn merge(&self, other: &Self) -> Self {
357        if !other.inherit {
358            return other.clone();
359        }
360        let mut merged = self.clone();
361        if other.has_explicit_family {
362            merged.family = other.family.clone();
363            merged.has_explicit_family = true;
364        }
365        if !other.fallback_families.is_empty() {
366            merged.fallback_families = other.fallback_families.clone();
367        }
368        if other.has_explicit_size {
369            merged.size = other.size;
370            merged.has_explicit_size = true;
371        }
372        if other.has_explicit_weight {
373            merged.weight = other.weight;
374            merged.has_explicit_weight = true;
375        }
376        if other.has_explicit_style {
377            merged.style = other.style;
378            merged.has_explicit_style = true;
379        }
380        if other.has_explicit_color || other.foreground.is_some() {
381            merged.color = other.color;
382            merged.has_explicit_color = true;
383        }
384        if other.background_color.is_some() {
385            merged.background_color = other.background_color;
386        }
387        if other.line_height.is_some() {
388            merged.line_height = other.line_height;
389        }
390        if other.letter_spacing != 0.0 {
391            merged.letter_spacing = other.letter_spacing;
392        }
393        if other.word_spacing.is_some() {
394            merged.word_spacing = other.word_spacing;
395        }
396        if other.text_baseline.is_some() {
397            merged.text_baseline = other.text_baseline;
398        }
399        if other.leading_distribution.is_some() {
400            merged.leading_distribution = other.leading_distribution;
401        }
402        if other.locale.is_some() {
403            merged.locale = other.locale.clone();
404        }
405        if other.foreground.is_some() {
406            merged.foreground = other.foreground;
407        }
408        if other.background.is_some() {
409            merged.background = other.background;
410        }
411        if !other.shadows.is_empty() {
412            merged.shadows = other.shadows.clone();
413        }
414        if other.font_variations.is_some() {
415            merged.font_variations = other.font_variations.clone();
416        }
417        if other.font_features.is_some() {
418            merged.font_features = other.font_features.clone();
419        }
420        if other.decoration.is_some() {
421            merged.decoration = other.decoration;
422        }
423        if other.decoration_color.is_some() {
424            merged.decoration_color = other.decoration_color;
425        }
426        if other.decoration_style.is_some() {
427            merged.decoration_style = other.decoration_style;
428        }
429        if other.decoration_thickness.is_some() {
430            merged.decoration_thickness = other.decoration_thickness;
431        }
432        if other.overflow.is_some() {
433            merged.overflow = other.overflow;
434        }
435        merged
436    }
437
438    #[must_use]
439    pub fn apply(
440        &self,
441        color: Option<Color>,
442        background_color: Option<Color>,
443        font_size_factor: Option<f32>,
444        font_size_delta: Option<f32>,
445        letter_spacing_factor: Option<f32>,
446        letter_spacing_delta: Option<f32>,
447    ) -> Self {
448        let mut res = self.clone();
449        if let Some(c) = color {
450            res.color = c;
451        }
452        if let Some(bg) = background_color {
453            res.background_color = Some(bg);
454        }
455        if let Some(factor) = font_size_factor {
456            res.size = (res.size * factor).max(0.0);
457        }
458        if let Some(delta) = font_size_delta {
459            res.size = (res.size + delta).max(0.0);
460        }
461        if let Some(factor) = letter_spacing_factor {
462            res.letter_spacing *= factor;
463        }
464        if let Some(delta) = letter_spacing_delta {
465            res.letter_spacing += delta;
466        }
467        res
468    }
469
470    #[must_use]
471    pub fn scaled(&self, scaler: TextScaler) -> Self {
472        let mut scaled = self.clone();
473        scaled.size = scaler.scale(self.size);
474        scaled
475    }
476
477    #[must_use]
478    pub fn change_impact(&self, other: &Self) -> ChangeImpact {
479        if self == other {
480            return ChangeImpact::None;
481        }
482        if self.family != other.family
483            || self.fallback_families != other.fallback_families
484            || self.size != other.size
485            || self.weight != other.weight
486            || self.style != other.style
487            || self.line_height != other.line_height
488            || self.letter_spacing != other.letter_spacing
489            || self.word_spacing != other.word_spacing
490            || self.text_baseline != other.text_baseline
491            || self.leading_distribution != other.leading_distribution
492            || self.font_variations != other.font_variations
493            || self.font_features != other.font_features
494            || self.overflow != other.overflow
495        {
496            ChangeImpact::Layout
497        } else {
498            ChangeImpact::Paint
499        }
500    }
501
502    #[must_use]
503    pub fn invalidation(&self, other: &Self) -> incular_core::Invalidation {
504        if self == other {
505            return incular_core::Invalidation::NONE;
506        }
507        if self.family != other.family
508            || self.fallback_families != other.fallback_families
509            || self.size != other.size
510            || self.weight != other.weight
511            || self.style != other.style
512            || self.line_height != other.line_height
513            || self.letter_spacing != other.letter_spacing
514            || self.word_spacing != other.word_spacing
515            || self.text_baseline != other.text_baseline
516            || self.leading_distribution != other.leading_distribution
517            || self.font_variations != other.font_variations
518            || self.font_features != other.font_features
519            || self.overflow != other.overflow
520        {
521            incular_core::Invalidation::LAYOUT | incular_core::Invalidation::PAINT
522        } else {
523            incular_core::Invalidation::PAINT
524        }
525    }
526}
527
528impl Lerp for TextStyle {
529    fn lerp(&self, other: &Self, t: f32) -> Self {
530        let t = t.clamp(0.0, 1.0);
531        let mix = |left: f32, right: f32| left + (right - left) * t;
532        Self {
533            inherit: if t < 0.5 { self.inherit } else { other.inherit },
534            has_explicit_color: self.has_explicit_color || other.has_explicit_color,
535            has_explicit_size: self.has_explicit_size || other.has_explicit_size,
536            has_explicit_weight: self.has_explicit_weight || other.has_explicit_weight,
537            has_explicit_style: self.has_explicit_style || other.has_explicit_style,
538            has_explicit_family: self.has_explicit_family || other.has_explicit_family,
539            family: if t < 0.5 {
540                self.family.clone()
541            } else {
542                other.family.clone()
543            },
544            fallback_families: if t < 0.5 {
545                self.fallback_families.clone()
546            } else {
547                other.fallback_families.clone()
548            },
549            size: mix(self.size, other.size),
550            weight: self.weight.lerp(&other.weight, t),
551            style: if t < 0.5 { self.style } else { other.style },
552            color: self.color.lerp(&other.color, t),
553            background_color: match (self.background_color, other.background_color) {
554                (Some(a), Some(b)) => Some(a.lerp(&b, t)),
555                (Some(a), None) => {
556                    if t < 0.5 {
557                        Some(a)
558                    } else {
559                        None
560                    }
561                }
562                (None, Some(b)) => {
563                    if t >= 0.5 {
564                        Some(b)
565                    } else {
566                        None
567                    }
568                }
569                (None, None) => None,
570            },
571            line_height: match (self.line_height, other.line_height) {
572                (Some(LineHeight::Multiplier(a)), Some(LineHeight::Multiplier(b))) => {
573                    Some(LineHeight::Multiplier(mix(a, b)))
574                }
575                (Some(LineHeight::Absolute(a)), Some(LineHeight::Absolute(b))) => {
576                    Some(LineHeight::Absolute(mix(a, b)))
577                }
578                (Some(a), Some(b)) => {
579                    if t < 0.5 {
580                        Some(a)
581                    } else {
582                        Some(b)
583                    }
584                }
585                (Some(a), None) => {
586                    if t < 0.5 {
587                        Some(a)
588                    } else {
589                        None
590                    }
591                }
592                (None, Some(b)) => {
593                    if t >= 0.5 {
594                        Some(b)
595                    } else {
596                        None
597                    }
598                }
599                (None, None) => None,
600            },
601            letter_spacing: mix(self.letter_spacing, other.letter_spacing),
602            word_spacing: match (self.word_spacing, other.word_spacing) {
603                (Some(left), Some(right)) => Some(mix(left, right)),
604                (Some(left), None) => Some(mix(left, 0.0)),
605                (None, Some(right)) => Some(mix(0.0, right)),
606                (None, None) => None,
607            },
608            text_baseline: if t < 0.5 {
609                self.text_baseline
610            } else {
611                other.text_baseline
612            },
613            leading_distribution: if t < 0.5 {
614                self.leading_distribution
615            } else {
616                other.leading_distribution
617            },
618            locale: if t < 0.5 {
619                self.locale.clone()
620            } else {
621                other.locale.clone()
622            },
623            foreground: match (self.foreground, other.foreground) {
624                (Some(a), Some(b)) => Some(a.lerp(&b, t)),
625                (Some(a), None) => {
626                    if t < 0.5 {
627                        Some(a)
628                    } else {
629                        None
630                    }
631                }
632                (None, Some(b)) => {
633                    if t >= 0.5 {
634                        Some(b)
635                    } else {
636                        None
637                    }
638                }
639                (None, None) => None,
640            },
641            background: match (self.background, other.background) {
642                (Some(a), Some(b)) => Some(a.lerp(&b, t)),
643                (Some(a), None) => {
644                    if t < 0.5 {
645                        Some(a)
646                    } else {
647                        None
648                    }
649                }
650                (None, Some(b)) => {
651                    if t >= 0.5 {
652                        Some(b)
653                    } else {
654                        None
655                    }
656                }
657                (None, None) => None,
658            },
659            shadows: if t < 0.5 {
660                self.shadows.clone()
661            } else {
662                other.shadows.clone()
663            },
664            font_variations: if t < 0.5 {
665                self.font_variations.clone()
666            } else {
667                other.font_variations.clone()
668            },
669            font_features: if t < 0.5 {
670                self.font_features.clone()
671            } else {
672                other.font_features.clone()
673            },
674            decoration: if t < 0.5 {
675                self.decoration
676            } else {
677                other.decoration
678            },
679            decoration_color: match (self.decoration_color, other.decoration_color) {
680                (Some(a), Some(b)) => Some(a.lerp(&b, t)),
681                (Some(a), None) => {
682                    if t < 0.5 {
683                        Some(a)
684                    } else {
685                        None
686                    }
687                }
688                (None, Some(b)) => {
689                    if t >= 0.5 {
690                        Some(b)
691                    } else {
692                        None
693                    }
694                }
695                (None, None) => None,
696            },
697            decoration_style: if t < 0.5 {
698                self.decoration_style
699            } else {
700                other.decoration_style
701            },
702            decoration_thickness: match (self.decoration_thickness, other.decoration_thickness) {
703                (Some(a), Some(b)) => Some(mix(a, b)),
704                (Some(a), None) => Some(mix(a, 0.0)),
705                (None, Some(b)) => Some(mix(0.0, b)),
706                (None, None) => None,
707            },
708            debug_label: if t < 0.5 {
709                self.debug_label.clone()
710            } else {
711                other.debug_label.clone()
712            },
713            overflow: if t < 0.5 {
714                self.overflow
715            } else {
716                other.overflow
717            },
718        }
719    }
720}
721
722impl Default for TextStyle {
723    fn default() -> Self {
724        Self {
725            inherit: true,
726            has_explicit_color: false,
727            has_explicit_size: false,
728            has_explicit_weight: false,
729            has_explicit_style: false,
730            has_explicit_family: false,
731            family: FontFamily::SystemUi,
732            fallback_families: Arc::new([]),
733            size: WidgetDefaults::DEFAULT.text_size,
734            weight: FontWeight::NORMAL,
735            style: FontStyle::Normal,
736            color: Color::WHITE,
737            background_color: None,
738            line_height: None,
739            letter_spacing: 0.0,
740            word_spacing: None,
741            text_baseline: None,
742            leading_distribution: None,
743            locale: None,
744            foreground: None,
745            background: None,
746            shadows: Arc::new([]),
747            font_variations: None,
748            font_features: None,
749            decoration: None,
750            decoration_color: None,
751            decoration_style: None,
752            decoration_thickness: None,
753            debug_label: None,
754            overflow: None,
755        }
756    }
757}