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 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 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 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 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 fn b(&self) -> u8 {
58        (self.0 & 0x00_00_00_FF) as u8
59    }
60
61    /// Get the alpha component
62    #[inline]
63    pub 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) => FamilyOwned::Name(SmolStr::from(name)),
83            Family::Serif => FamilyOwned::Serif,
84            Family::SansSerif => FamilyOwned::SansSerif,
85            Family::Cursive => FamilyOwned::Cursive,
86            Family::Fantasy => FamilyOwned::Fantasy,
87            Family::Monospace => FamilyOwned::Monospace,
88        }
89    }
90
91    pub fn as_family(&self) -> Family {
92        match self {
93            FamilyOwned::Name(name) => Family::Name(name),
94            FamilyOwned::Serif => Family::Serif,
95            FamilyOwned::SansSerif => Family::SansSerif,
96            FamilyOwned::Cursive => Family::Cursive,
97            FamilyOwned::Fantasy => Family::Fantasy,
98            FamilyOwned::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, PartialEq)]
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 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    pub features: Vec<Feature>,
170}
171
172impl FontFeatures {
173    pub fn new() -> Self {
174        Self {
175            features: Vec::new(),
176        }
177    }
178
179    pub fn set(&mut self, tag: FeatureTag, value: u32) -> &mut Self {
180        self.features.push(Feature { tag, value });
181        self
182    }
183
184    /// Enable a feature (set to 1)
185    pub fn enable(&mut self, tag: FeatureTag) -> &mut Self {
186        self.set(tag, 1)
187    }
188
189    /// Disable a feature (set to 0)
190    pub fn disable(&mut self, tag: FeatureTag) -> &mut Self {
191        self.set(tag, 0)
192    }
193}
194
195/// A wrapper for letter spacing to get around that f32 doesn't implement Eq and Hash
196#[derive(Clone, Copy, Debug)]
197pub struct LetterSpacing(pub f32);
198
199impl PartialEq for LetterSpacing {
200    fn eq(&self, other: &Self) -> bool {
201        if self.0.is_nan() {
202            other.0.is_nan()
203        } else {
204            self.0 == other.0
205        }
206    }
207}
208
209impl Eq for LetterSpacing {}
210
211impl Hash for LetterSpacing {
212    fn hash<H: Hasher>(&self, hasher: &mut H) {
213        const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
214
215        let bits = if self.0.is_nan() {
216            CANONICAL_NAN_BITS
217        } else {
218            // Add +0.0 to canonicalize -0.0 to +0.0
219            (self.0 + 0.0).to_bits()
220        };
221
222        bits.hash(hasher);
223    }
224}
225
226/// Text attributes
227#[derive(Clone, Debug, Eq, Hash, PartialEq)]
228pub struct Attrs<'a> {
229    //TODO: should this be an option?
230    pub color_opt: Option<Color>,
231    pub family: Family<'a>,
232    pub stretch: Stretch,
233    pub style: Style,
234    pub weight: Weight,
235    pub metadata: usize,
236    pub cache_key_flags: CacheKeyFlags,
237    pub metrics_opt: Option<CacheMetrics>,
238    /// Letter spacing (tracking) in EM
239    pub letter_spacing_opt: Option<LetterSpacing>,
240    pub font_features: FontFeatures,
241}
242
243impl<'a> Attrs<'a> {
244    /// Create a new set of attributes with sane defaults
245    ///
246    /// This defaults to a regular Sans-Serif font.
247    pub fn new() -> Self {
248        Self {
249            color_opt: None,
250            family: Family::SansSerif,
251            stretch: Stretch::Normal,
252            style: Style::Normal,
253            weight: Weight::NORMAL,
254            metadata: 0,
255            cache_key_flags: CacheKeyFlags::empty(),
256            metrics_opt: None,
257            letter_spacing_opt: None,
258            font_features: FontFeatures::new(),
259        }
260    }
261
262    /// Set [Color]
263    pub fn color(mut self, color: Color) -> Self {
264        self.color_opt = Some(color);
265        self
266    }
267
268    /// Set [Family]
269    pub fn family(mut self, family: Family<'a>) -> Self {
270        self.family = family;
271        self
272    }
273
274    /// Set [Stretch]
275    pub fn stretch(mut self, stretch: Stretch) -> Self {
276        self.stretch = stretch;
277        self
278    }
279
280    /// Set [Style]
281    pub fn style(mut self, style: Style) -> Self {
282        self.style = style;
283        self
284    }
285
286    /// Set [Weight]
287    pub fn weight(mut self, weight: Weight) -> Self {
288        self.weight = weight;
289        self
290    }
291
292    /// Set metadata
293    pub fn metadata(mut self, metadata: usize) -> Self {
294        self.metadata = metadata;
295        self
296    }
297
298    /// Set [`CacheKeyFlags`]
299    pub fn cache_key_flags(mut self, cache_key_flags: CacheKeyFlags) -> Self {
300        self.cache_key_flags = cache_key_flags;
301        self
302    }
303
304    /// Set [`Metrics`], overriding values in buffer
305    pub fn metrics(mut self, metrics: Metrics) -> Self {
306        self.metrics_opt = Some(metrics.into());
307        self
308    }
309
310    /// Set letter spacing (tracking) in EM
311    pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
312        self.letter_spacing_opt = Some(LetterSpacing(letter_spacing));
313        self
314    }
315
316    /// Set [`FontFeatures`]
317    pub fn font_features(mut self, font_features: FontFeatures) -> Self {
318        self.font_features = font_features;
319        self
320    }
321
322    /// Check if font matches
323    pub fn matches(&self, face: &fontdb::FaceInfo) -> bool {
324        //TODO: smarter way of including emoji
325        face.post_script_name.contains("Emoji")
326            || (face.style == self.style && face.stretch == self.stretch)
327    }
328
329    /// Check if this set of attributes can be shaped with another
330    pub fn compatible(&self, other: &Self) -> bool {
331        self.family == other.family
332            && self.stretch == other.stretch
333            && self.style == other.style
334            && self.weight == other.weight
335    }
336}
337
338/// Font-specific part of [`Attrs`] to be used for matching
339#[derive(Clone, Debug, Eq, Hash, PartialEq)]
340pub struct FontMatchAttrs {
341    family: FamilyOwned,
342    stretch: Stretch,
343    style: Style,
344    weight: Weight,
345}
346
347impl<'a> From<&Attrs<'a>> for FontMatchAttrs {
348    fn from(attrs: &Attrs<'a>) -> Self {
349        Self {
350            family: FamilyOwned::new(attrs.family),
351            stretch: attrs.stretch,
352            style: attrs.style,
353            weight: attrs.weight,
354        }
355    }
356}
357
358/// An owned version of [`Attrs`]
359#[derive(Clone, Debug, Eq, Hash, PartialEq)]
360pub struct AttrsOwned {
361    //TODO: should this be an option?
362    pub color_opt: Option<Color>,
363    pub family_owned: FamilyOwned,
364    pub stretch: Stretch,
365    pub style: Style,
366    pub weight: Weight,
367    pub metadata: usize,
368    pub cache_key_flags: CacheKeyFlags,
369    pub metrics_opt: Option<CacheMetrics>,
370    /// Letter spacing (tracking) in EM
371    pub letter_spacing_opt: Option<LetterSpacing>,
372    pub font_features: FontFeatures,
373}
374
375impl AttrsOwned {
376    pub fn new(attrs: &Attrs) -> Self {
377        Self {
378            color_opt: attrs.color_opt,
379            family_owned: FamilyOwned::new(attrs.family),
380            stretch: attrs.stretch,
381            style: attrs.style,
382            weight: attrs.weight,
383            metadata: attrs.metadata,
384            cache_key_flags: attrs.cache_key_flags,
385            metrics_opt: attrs.metrics_opt,
386            letter_spacing_opt: attrs.letter_spacing_opt,
387            font_features: attrs.font_features.clone(),
388        }
389    }
390
391    pub fn as_attrs(&self) -> Attrs {
392        Attrs {
393            color_opt: self.color_opt,
394            family: self.family_owned.as_family(),
395            stretch: self.stretch,
396            style: self.style,
397            weight: self.weight,
398            metadata: self.metadata,
399            cache_key_flags: self.cache_key_flags,
400            metrics_opt: self.metrics_opt,
401            letter_spacing_opt: self.letter_spacing_opt,
402            font_features: self.font_features.clone(),
403        }
404    }
405}
406
407/// List of text attributes to apply to a line
408//TODO: have this clean up the spans when changes are made
409#[derive(Debug, Clone, Eq, PartialEq)]
410pub struct AttrsList {
411    defaults: AttrsOwned,
412    pub(crate) spans: RangeMap<usize, AttrsOwned>,
413}
414
415impl AttrsList {
416    /// Create a new attributes list with a set of default [Attrs]
417    pub fn new(defaults: &Attrs) -> Self {
418        Self {
419            defaults: AttrsOwned::new(defaults),
420            spans: RangeMap::new(),
421        }
422    }
423
424    /// Get the default [Attrs]
425    pub fn defaults(&self) -> Attrs {
426        self.defaults.as_attrs()
427    }
428
429    /// Get the current attribute spans
430    pub fn spans(&self) -> Vec<(&Range<usize>, &AttrsOwned)> {
431        self.spans_iter().collect()
432    }
433
434    /// Get an iterator over the current attribute spans
435    pub fn spans_iter(&self) -> impl Iterator<Item = (&Range<usize>, &AttrsOwned)> + '_ {
436        self.spans.iter()
437    }
438
439    /// Clear the current attribute spans
440    pub fn clear_spans(&mut self) {
441        self.spans.clear();
442    }
443
444    /// Add an attribute span, removes any previous matching parts of spans
445    pub fn add_span(&mut self, range: Range<usize>, attrs: &Attrs) {
446        //do not support 1..1 or 2..1 even if by accident.
447        if range.is_empty() {
448            return;
449        }
450
451        self.spans.insert(range, AttrsOwned::new(attrs));
452    }
453
454    /// Get the attribute span for an index
455    ///
456    /// This returns a span that contains the index
457    pub fn get_span(&self, index: usize) -> Attrs {
458        self.spans
459            .get(&index)
460            .map(|v| v.as_attrs())
461            .unwrap_or(self.defaults.as_attrs())
462    }
463
464    /// Split attributes list at an offset
465    #[allow(clippy::missing_panics_doc)]
466    pub fn split_off(&mut self, index: usize) -> Self {
467        let mut new = Self::new(&self.defaults.as_attrs());
468        let mut removes = Vec::new();
469
470        //get the keys we need to remove or fix.
471        for span in self.spans.iter() {
472            if span.0.end <= index {
473                continue;
474            } else if span.0.start >= index {
475                removes.push((span.0.clone(), false));
476            } else {
477                removes.push((span.0.clone(), true));
478            }
479        }
480
481        for (key, resize) in removes {
482            let (range, attrs) = self
483                .spans
484                .get_key_value(&key.start)
485                .map(|v| (v.0.clone(), v.1.clone()))
486                .expect("attrs span not found");
487            self.spans.remove(key);
488
489            if resize {
490                new.spans.insert(0..range.end - index, attrs.clone());
491                self.spans.insert(range.start..index, attrs);
492            } else {
493                new.spans
494                    .insert(range.start - index..range.end - index, attrs);
495            }
496        }
497        new
498    }
499
500    /// Resets the attributes with new defaults.
501    pub(crate) fn reset(mut self, default: &Attrs) -> Self {
502        self.defaults = AttrsOwned::new(default);
503        self.spans.clear();
504        self
505    }
506}