Skip to main content

cosmic_text/
attrs.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5use core::hash::{Hash, Hasher};
6use core::ops::Range;
7use rangemap::RangeMap;
8use smol_str::SmolStr;
9
10use crate::{CacheKeyFlags, Metrics};
11
12pub use fontdb::{Family, Stretch, Style, Weight};
13
14/// Text color
15#[derive(Clone, Copy, Debug, PartialOrd, Ord, Eq, Hash, PartialEq)]
16pub struct Color(pub u32);
17
18impl Color {
19    /// Create new color with red, green, and blue components
20    #[inline]
21    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
22        Self::rgba(r, g, b, 0xFF)
23    }
24
25    /// Create new color with red, green, blue, and alpha components
26    #[inline]
27    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
28        Self(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32))
29    }
30
31    /// Get a tuple over all of the attributes, in `(r, g, b, a)` order.
32    #[inline]
33    pub const fn as_rgba_tuple(self) -> (u8, u8, u8, u8) {
34        (self.r(), self.g(), self.b(), self.a())
35    }
36
37    /// Get an array over all of the components, in `[r, g, b, a]` order.
38    #[inline]
39    pub const fn as_rgba(self) -> [u8; 4] {
40        [self.r(), self.g(), self.b(), self.a()]
41    }
42
43    /// Get the red component
44    #[inline]
45    pub const fn r(&self) -> u8 {
46        ((self.0 & 0x00_FF_00_00) >> 16) as u8
47    }
48
49    /// Get the green component
50    #[inline]
51    pub const fn g(&self) -> u8 {
52        ((self.0 & 0x00_00_FF_00) >> 8) as u8
53    }
54
55    /// Get the blue component
56    #[inline]
57    pub const fn b(&self) -> u8 {
58        (self.0 & 0x00_00_00_FF) as u8
59    }
60
61    /// Get the alpha component
62    #[inline]
63    pub const fn a(&self) -> u8 {
64        ((self.0 & 0xFF_00_00_00) >> 24) as u8
65    }
66}
67
68/// An owned version of [`Family`]
69#[derive(Clone, Debug, Eq, Hash, PartialEq)]
70pub enum FamilyOwned {
71    Name(SmolStr),
72    Serif,
73    SansSerif,
74    Cursive,
75    Fantasy,
76    Monospace,
77}
78
79impl FamilyOwned {
80    pub fn new(family: Family) -> Self {
81        match family {
82            Family::Name(name) => Self::Name(SmolStr::from(name)),
83            Family::Serif => Self::Serif,
84            Family::SansSerif => Self::SansSerif,
85            Family::Cursive => Self::Cursive,
86            Family::Fantasy => Self::Fantasy,
87            Family::Monospace => Self::Monospace,
88        }
89    }
90
91    pub fn as_family(&self) -> Family<'_> {
92        match self {
93            Self::Name(name) => Family::Name(name),
94            Self::Serif => Family::Serif,
95            Self::SansSerif => Family::SansSerif,
96            Self::Cursive => Family::Cursive,
97            Self::Fantasy => Family::Fantasy,
98            Self::Monospace => Family::Monospace,
99        }
100    }
101}
102
103/// Metrics, but implementing Eq and Hash using u32 representation of f32
104//TODO: what are the edge cases of this?
105#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
106pub struct CacheMetrics {
107    font_size_bits: u32,
108    line_height_bits: u32,
109}
110
111impl From<Metrics> for CacheMetrics {
112    fn from(metrics: Metrics) -> Self {
113        Self {
114            font_size_bits: metrics.font_size.to_bits(),
115            line_height_bits: metrics.line_height.to_bits(),
116        }
117    }
118}
119
120impl From<CacheMetrics> for Metrics {
121    fn from(metrics: CacheMetrics) -> Self {
122        Self {
123            font_size: f32::from_bits(metrics.font_size_bits),
124            line_height: f32::from_bits(metrics.line_height_bits),
125        }
126    }
127}
128/// A 4-byte `OpenType` feature tag identifier
129#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub struct FeatureTag([u8; 4]);
131
132impl FeatureTag {
133    pub const fn new(tag: &[u8; 4]) -> Self {
134        Self(*tag)
135    }
136
137    /// Kerning adjusts spacing between specific character pairs
138    pub const KERNING: Self = Self::new(b"kern");
139    /// Standard ligatures (fi, fl, etc.)
140    pub const STANDARD_LIGATURES: Self = Self::new(b"liga");
141    /// Contextual ligatures (context-dependent ligatures)
142    pub const CONTEXTUAL_LIGATURES: Self = Self::new(b"clig");
143    /// Contextual alternates (glyph substitutions based on context)
144    pub const CONTEXTUAL_ALTERNATES: Self = Self::new(b"calt");
145    /// Discretionary ligatures (optional stylistic ligatures)
146    pub const DISCRETIONARY_LIGATURES: Self = Self::new(b"dlig");
147    /// Small caps (lowercase to small capitals)
148    pub const SMALL_CAPS: Self = Self::new(b"smcp");
149    /// All small caps (uppercase and lowercase to small capitals)
150    pub const ALL_SMALL_CAPS: Self = Self::new(b"c2sc");
151    /// Stylistic Set 1 (font-specific alternate glyphs)
152    pub const STYLISTIC_SET_1: Self = Self::new(b"ss01");
153    /// Stylistic Set 2 (font-specific alternate glyphs)
154    pub const STYLISTIC_SET_2: Self = Self::new(b"ss02");
155
156    pub const fn as_bytes(&self) -> &[u8; 4] {
157        &self.0
158    }
159}
160
161#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
162pub struct Feature {
163    pub tag: FeatureTag,
164    pub value: u32,
165}
166
167#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
168pub struct FontFeatures {
169    features: Vec<Feature>,
170}
171
172impl FontFeatures {
173    pub const fn new() -> Self {
174        Self {
175            features: Vec::new(),
176        }
177    }
178
179    pub fn iter(&self) -> impl Iterator<Item = &Feature> {
180        self.features.iter()
181    }
182
183    /// Set a feature value by [`FeatureTag`].
184    ///
185    /// This ensures that the features are sorted by [`FeatureTag`] without duplicate tags.
186    pub fn set(&mut self, tag: FeatureTag, value: u32) -> &mut Self {
187        match self
188            .features
189            .binary_search_by_key(&tag, |feature| feature.tag)
190        {
191            Ok(index) => self.features[index].value = value,
192            Err(index) => self.features.insert(index, Feature { tag, value }),
193        }
194        self
195    }
196
197    /// Enable a feature (set to 1)
198    pub fn enable(&mut self, tag: FeatureTag) -> &mut Self {
199        self.set(tag, 1)
200    }
201
202    /// Disable a feature (set to 0)
203    pub fn disable(&mut self, tag: FeatureTag) -> &mut Self {
204        self.set(tag, 0)
205    }
206}
207
208/// A wrapper for letter spacing to get around that f32 doesn't implement Eq and Hash
209#[derive(Clone, Copy, Debug)]
210pub struct LetterSpacing(pub f32);
211
212impl PartialEq for LetterSpacing {
213    fn eq(&self, other: &Self) -> bool {
214        if self.0.is_nan() {
215            other.0.is_nan()
216        } else {
217            self.0 == other.0
218        }
219    }
220}
221
222impl Eq for LetterSpacing {}
223
224impl Hash for LetterSpacing {
225    fn hash<H: Hasher>(&self, hasher: &mut H) {
226        const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
227
228        let bits = if self.0.is_nan() {
229            CANONICAL_NAN_BITS
230        } else {
231            // Add +0.0 to canonicalize -0.0 to +0.0
232            (self.0 + 0.0).to_bits()
233        };
234
235        bits.hash(hasher);
236    }
237}
238
239#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
240pub enum UnderlineStyle {
241    #[default]
242    None,
243    Single,
244    Double,
245    // TODO: Wavy
246}
247
248#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
249pub struct TextDecoration {
250    pub underline: UnderlineStyle,
251    pub underline_color_opt: Option<Color>,
252    pub strikethrough: bool,
253    pub strikethrough_color_opt: Option<Color>,
254    pub overline: bool,
255    pub overline_color_opt: Option<Color>,
256}
257
258impl TextDecoration {
259    pub const fn new() -> Self {
260        Self {
261            underline: UnderlineStyle::None,
262            underline_color_opt: None,
263            strikethrough: false,
264            strikethrough_color_opt: None,
265            overline: false,
266            overline_color_opt: None,
267        }
268    }
269
270    pub const fn has_decoration(&self) -> bool {
271        !matches!(self.underline, UnderlineStyle::None) || self.strikethrough || self.overline
272    }
273}
274
275/// Offset and thickness for a text decoration line, in EM units.
276#[derive(Clone, Copy, Debug, Default, PartialEq)]
277pub struct DecorationMetrics {
278    /// Offset from baseline in EM units
279    pub offset: f32,
280    /// Thickness in EM units
281    pub thickness: f32,
282}
283
284#[derive(Clone, Debug, PartialEq)]
285pub struct GlyphDecorationData {
286    /// The text decoration configuration from the user
287    pub text_decoration: TextDecoration,
288    /// Underline offset and thickness from the font
289    pub underline_metrics: DecorationMetrics,
290    /// Strikethrough offset and thickness from the font
291    pub strikethrough_metrics: DecorationMetrics,
292    /// Font ascent in EM units (ascent / upem).
293    /// Used for overline positioning
294    pub ascent: f32,
295}
296
297/// Text attributes
298#[derive(Clone, Debug, Eq, Hash, PartialEq)]
299pub struct Attrs<'a> {
300    //TODO: should this be an option?
301    pub color_opt: Option<Color>,
302    pub family: Family<'a>,
303    pub stretch: Stretch,
304    pub style: Style,
305    pub weight: Weight,
306    pub metadata: usize,
307    pub cache_key_flags: CacheKeyFlags,
308    pub metrics_opt: Option<CacheMetrics>,
309    /// Letter spacing (tracking) in EM
310    pub letter_spacing_opt: Option<LetterSpacing>,
311    pub font_features: FontFeatures,
312    pub text_decoration: TextDecoration,
313}
314
315impl<'a> Attrs<'a> {
316    /// Create a new set of attributes with sane defaults
317    ///
318    /// This defaults to a regular Sans-Serif font.
319    pub const fn new() -> Self {
320        Self {
321            color_opt: None,
322            family: Family::SansSerif,
323            stretch: Stretch::Normal,
324            style: Style::Normal,
325            weight: Weight::NORMAL,
326            metadata: 0,
327            cache_key_flags: CacheKeyFlags::empty(),
328            metrics_opt: None,
329            letter_spacing_opt: None,
330            font_features: FontFeatures::new(),
331            text_decoration: TextDecoration::new(),
332        }
333    }
334
335    /// Set [Color]
336    pub const fn color(mut self, color: Color) -> Self {
337        self.color_opt = Some(color);
338        self
339    }
340
341    /// Set [Family]
342    pub const fn family(mut self, family: Family<'a>) -> Self {
343        self.family = family;
344        self
345    }
346
347    /// Set [Stretch]
348    pub const fn stretch(mut self, stretch: Stretch) -> Self {
349        self.stretch = stretch;
350        self
351    }
352
353    /// Set [Style]
354    pub const fn style(mut self, style: Style) -> Self {
355        self.style = style;
356        self
357    }
358
359    /// Set [Weight]
360    pub const fn weight(mut self, weight: Weight) -> Self {
361        self.weight = weight;
362        self
363    }
364
365    /// Set metadata
366    pub const fn metadata(mut self, metadata: usize) -> Self {
367        self.metadata = metadata;
368        self
369    }
370
371    /// Set [`CacheKeyFlags`]
372    pub const fn cache_key_flags(mut self, cache_key_flags: CacheKeyFlags) -> Self {
373        self.cache_key_flags = cache_key_flags;
374        self
375    }
376
377    /// Set [`Metrics`], overriding values in buffer
378    pub fn metrics(mut self, metrics: Metrics) -> Self {
379        self.metrics_opt = Some(metrics.into());
380        self
381    }
382
383    /// Set letter spacing (tracking) in EM
384    pub const fn letter_spacing(mut self, letter_spacing: f32) -> Self {
385        self.letter_spacing_opt = Some(LetterSpacing(letter_spacing));
386        self
387    }
388
389    /// Set [`FontFeatures`]
390    pub fn font_features(mut self, font_features: FontFeatures) -> Self {
391        self.font_features = font_features;
392        self
393    }
394
395    pub const fn underline(mut self, style: UnderlineStyle) -> Self {
396        self.text_decoration.underline = style;
397        self
398    }
399
400    pub const fn underline_color(mut self, color: Color) -> Self {
401        self.text_decoration.underline_color_opt = Some(color);
402        self
403    }
404
405    pub const fn strikethrough(mut self) -> Self {
406        self.text_decoration.strikethrough = true;
407        self
408    }
409
410    pub const fn strikethrough_color(mut self, color: Color) -> Self {
411        self.text_decoration.strikethrough_color_opt = Some(color);
412        self
413    }
414
415    pub const fn overline(mut self) -> Self {
416        self.text_decoration.overline = true;
417        self
418    }
419
420    pub const fn overline_color(mut self, color: Color) -> Self {
421        self.text_decoration.overline_color_opt = Some(color);
422        self
423    }
424
425    /// Check if this set of attributes can be shaped with another
426    pub fn compatible(&self, other: &Self) -> bool {
427        self.family == other.family
428            && self.stretch == other.stretch
429            && self.style == other.style
430            && self.weight == other.weight
431            && self.font_features == other.font_features
432    }
433}
434
435/// Font-specific part of [`Attrs`] to be used for matching
436#[derive(Clone, Debug, Eq, Hash, PartialEq)]
437pub struct FontMatchAttrs {
438    family: FamilyOwned,
439    stretch: Stretch,
440    style: Style,
441    weight: Weight,
442}
443
444impl<'a> From<&Attrs<'a>> for FontMatchAttrs {
445    fn from(attrs: &Attrs<'a>) -> Self {
446        Self {
447            family: FamilyOwned::new(attrs.family),
448            stretch: attrs.stretch,
449            style: attrs.style,
450            weight: attrs.weight,
451        }
452    }
453}
454
455/// An owned version of [`Attrs`]
456#[derive(Clone, Debug, Eq, Hash, PartialEq)]
457pub struct AttrsOwned {
458    //TODO: should this be an option?
459    pub color_opt: Option<Color>,
460    pub family_owned: FamilyOwned,
461    pub stretch: Stretch,
462    pub style: Style,
463    pub weight: Weight,
464    pub metadata: usize,
465    pub cache_key_flags: CacheKeyFlags,
466    pub metrics_opt: Option<CacheMetrics>,
467    /// Letter spacing (tracking) in EM
468    pub letter_spacing_opt: Option<LetterSpacing>,
469    pub font_features: FontFeatures,
470    pub text_decoration: TextDecoration,
471}
472
473impl AttrsOwned {
474    /// Equal in every attribute that affects shaping, ignoring
475    /// `text_decoration`, which is resolved at layout.
476    pub fn eq_shape_attrs(&self, other: &Self) -> bool {
477        self.color_opt == other.color_opt
478            && self.family_owned == other.family_owned
479            && self.stretch == other.stretch
480            && self.style == other.style
481            && self.weight == other.weight
482            && self.metadata == other.metadata
483            && self.cache_key_flags == other.cache_key_flags
484            && self.metrics_opt == other.metrics_opt
485            && self.letter_spacing_opt == other.letter_spacing_opt
486            && self.font_features == other.font_features
487    }
488
489    pub fn new(attrs: &Attrs) -> Self {
490        Self {
491            color_opt: attrs.color_opt,
492            family_owned: FamilyOwned::new(attrs.family),
493            stretch: attrs.stretch,
494            style: attrs.style,
495            weight: attrs.weight,
496            metadata: attrs.metadata,
497            cache_key_flags: attrs.cache_key_flags,
498            metrics_opt: attrs.metrics_opt,
499            letter_spacing_opt: attrs.letter_spacing_opt,
500            font_features: attrs.font_features.clone(),
501            text_decoration: attrs.text_decoration,
502        }
503    }
504
505    pub fn as_attrs(&self) -> Attrs<'_> {
506        Attrs {
507            color_opt: self.color_opt,
508            family: self.family_owned.as_family(),
509            stretch: self.stretch,
510            style: self.style,
511            weight: self.weight,
512            metadata: self.metadata,
513            cache_key_flags: self.cache_key_flags,
514            metrics_opt: self.metrics_opt,
515            letter_spacing_opt: self.letter_spacing_opt,
516            font_features: self.font_features.clone(),
517            text_decoration: self.text_decoration,
518        }
519    }
520}
521
522/// List of text attributes to apply to a line
523//TODO: have this clean up the spans when changes are made
524#[derive(Debug, Clone, Eq, PartialEq)]
525pub struct AttrsList {
526    defaults: AttrsOwned,
527    pub(crate) spans: RangeMap<usize, AttrsOwned>,
528}
529
530impl AttrsList {
531    /// Create a new attributes list with a set of default [Attrs]
532    pub fn new(defaults: &Attrs) -> Self {
533        Self {
534            defaults: AttrsOwned::new(defaults),
535            spans: RangeMap::new(),
536        }
537    }
538
539    /// Get the default [Attrs]
540    pub fn defaults(&self) -> Attrs<'_> {
541        self.defaults.as_attrs()
542    }
543
544    /// True if the two lists differ only in `text_decoration`. Spans that carry
545    /// only decoration (otherwise equal to the defaults) are ignored.
546    pub fn eq_shape_attrs(&self, other: &Self) -> bool {
547        if !self.defaults.eq_shape_attrs(&other.defaults) {
548            return false;
549        }
550        let mut a = self
551            .spans_iter()
552            .filter(|(_, attrs)| !attrs.eq_shape_attrs(&self.defaults));
553        let mut b = other
554            .spans_iter()
555            .filter(|(_, attrs)| !attrs.eq_shape_attrs(&other.defaults));
556        loop {
557            match (a.next(), b.next()) {
558                (None, None) => return true,
559                (Some((ra, aa)), Some((rb, ab))) if ra == rb && aa.eq_shape_attrs(ab) => {}
560                _ => return false,
561            }
562        }
563    }
564
565    /// Get the current attribute spans
566    pub fn spans(&self) -> Vec<(&Range<usize>, &AttrsOwned)> {
567        self.spans_iter().collect()
568    }
569
570    /// Get an iterator over the current attribute spans
571    pub fn spans_iter(&self) -> impl Iterator<Item = (&Range<usize>, &AttrsOwned)> + '_ {
572        self.spans.iter()
573    }
574
575    /// Clear the current attribute spans
576    pub fn clear_spans(&mut self) {
577        self.spans.clear();
578    }
579
580    /// Add an attribute span, removes any previous matching parts of spans
581    pub fn add_span(&mut self, range: Range<usize>, attrs: &Attrs) {
582        //do not support 1..1 or 2..1 even if by accident.
583        if range.is_empty() {
584            return;
585        }
586
587        self.spans.insert(range, AttrsOwned::new(attrs));
588    }
589
590    /// Get the attribute span for an index
591    ///
592    /// This returns a span that contains the index
593    pub fn get_span(&self, index: usize) -> Attrs<'_> {
594        self.spans
595            .get(&index)
596            .map(|v| v.as_attrs())
597            .unwrap_or(self.defaults.as_attrs())
598    }
599
600    /// Split attributes list at an offset
601    #[allow(clippy::missing_panics_doc)]
602    pub fn split_off(&mut self, index: usize) -> Self {
603        let mut new = Self::new(&self.defaults.as_attrs());
604        let mut removes = Vec::new();
605
606        //get the keys we need to remove or fix.
607        for span in self.spans.iter() {
608            if span.0.end <= index {
609                continue;
610            }
611
612            if span.0.start >= index {
613                removes.push((span.0.clone(), false));
614            } else {
615                removes.push((span.0.clone(), true));
616            }
617        }
618
619        for (key, resize) in removes {
620            let (range, attrs) = self
621                .spans
622                .get_key_value(&key.start)
623                .map(|v| (v.0.clone(), v.1.clone()))
624                .expect("attrs span not found");
625            self.spans.remove(key);
626
627            if resize {
628                new.spans.insert(0..range.end - index, attrs.clone());
629                self.spans.insert(range.start..index, attrs);
630            } else {
631                new.spans
632                    .insert(range.start - index..range.end - index, attrs);
633            }
634        }
635        new
636    }
637
638    /// Resets the attributes with new defaults.
639    pub(crate) fn reset(mut self, default: &Attrs) -> Self {
640        self.defaults = AttrsOwned::new(default);
641        self.spans.clear();
642        self
643    }
644}