Skip to main content

floem_renderer/text/
attrs.rs

1use std::ops::Range;
2
3use crate::text::TextBrush;
4use crate::text::{FontStyle, FontWeight, FontWidth};
5use fontique::GenericFamily;
6use parley::style::{FontFamily, FontStack, StyleProperty, WordBreakStrength};
7use peniko::Color;
8
9/// An owned font family identifier.
10///
11/// This is an owned equivalent of Parley's [`FontFamily`] that can be stored
12/// and cloned independently of any layout context. It supports both named fonts
13/// and the standard CSS generic families.
14///
15/// # Example
16///
17/// ```
18/// use floem_renderer::text::FamilyOwned;
19///
20/// let families: Vec<FamilyOwned> = FamilyOwned::parse_list("'Fira Code', monospace").collect();
21/// assert_eq!(families, vec![
22///     FamilyOwned::Name("Fira Code".to_string()),
23///     FamilyOwned::Monospace,
24/// ]);
25/// ```
26///
27/// [`FontFamily`]: parley::style::FontFamily
28#[derive(Clone, Debug, Eq, Hash, PartialEq)]
29pub enum FamilyOwned {
30    /// A named font family (e.g. `"Helvetica"`, `"Fira Code"`).
31    Name(String),
32    /// The generic serif family.
33    Serif,
34    /// The generic sans-serif family.
35    SansSerif,
36    /// The generic cursive family.
37    Cursive,
38    /// The generic fantasy family.
39    Fantasy,
40    /// The generic monospace family.
41    Monospace,
42}
43
44impl FamilyOwned {
45    /// Parses a CSS-style comma-separated font family list into an iterator of [`FamilyOwned`] values.
46    ///
47    /// Quoted names (single or double quotes) are treated as named families.
48    /// Unquoted generic keywords (`serif`, `sans-serif`, `monospace`, `cursive`, `fantasy`)
49    /// are mapped to their corresponding variants. All other unquoted names become
50    /// [`FamilyOwned::Name`].
51    ///
52    /// # Example
53    ///
54    /// ```
55    /// use floem_renderer::text::FamilyOwned;
56    ///
57    /// let families: Vec<_> = FamilyOwned::parse_list("Arial, sans-serif").collect();
58    /// assert_eq!(families, vec![
59    ///     FamilyOwned::Name("Arial".to_string()),
60    ///     FamilyOwned::SansSerif,
61    /// ]);
62    /// ```
63    pub fn parse_list(s: &str) -> impl Iterator<Item = FamilyOwned> + '_ + Clone {
64        ParseList {
65            source: s.as_bytes(),
66            len: s.len(),
67            pos: 0,
68        }
69    }
70
71    /// Converts this owned family to a borrowed Parley [`FontFamily`] reference.
72    fn to_font_family(&self) -> FontFamily<'_> {
73        match self {
74            FamilyOwned::Name(name) => FontFamily::Named(std::borrow::Cow::Borrowed(name.as_str())),
75            FamilyOwned::Serif => FontFamily::Generic(GenericFamily::Serif),
76            FamilyOwned::SansSerif => FontFamily::Generic(GenericFamily::SansSerif),
77            FamilyOwned::Cursive => FontFamily::Generic(GenericFamily::Cursive),
78            FamilyOwned::Fantasy => FontFamily::Generic(GenericFamily::Fantasy),
79            FamilyOwned::Monospace => FontFamily::Generic(GenericFamily::Monospace),
80        }
81    }
82
83    /// Converts a slice of owned families into a Parley [`FontStack`].
84    ///
85    /// For a single named family, this produces a [`FontStack::Source`] so that
86    /// Parley can parse comma-separated fallbacks within the name string.
87    /// For a single generic family, it produces [`FontStack::Single`].
88    /// For multiple families, it produces [`FontStack::List`] preserving the
89    /// full fallback chain.
90    /// An empty slice defaults to sans-serif.
91    pub fn to_font_stack(families: &[FamilyOwned]) -> FontStack<'_> {
92        match families {
93            [] => FontStack::Single(FontFamily::Generic(GenericFamily::SansSerif)),
94            [single] => match single {
95                FamilyOwned::Name(name) => {
96                    FontStack::Source(std::borrow::Cow::Borrowed(name.as_str()))
97                }
98                other => FontStack::Single(other.to_font_family()),
99            },
100            multiple => {
101                let list: Vec<FontFamily<'_>> =
102                    multiple.iter().map(|f| f.to_font_family()).collect();
103                FontStack::List(std::borrow::Cow::Owned(list))
104            }
105        }
106    }
107}
108
109/// Specifies how line height is computed for text layout.
110///
111/// # Example
112///
113/// ```
114/// use floem_renderer::text::{Attrs, LineHeightValue};
115///
116/// // 1.5x the font size (e.g. 24px for a 16px font).
117/// let attrs = Attrs::new().line_height(LineHeightValue::Normal(1.5));
118///
119/// // Fixed 20-point line height regardless of font size.
120/// let attrs = Attrs::new().line_height(LineHeightValue::Pt(20.0));
121/// ```
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub enum LineHeightValue {
124    /// A multiplier of the font size (e.g. `1.0` means line height equals font size).
125    Normal(f32),
126    /// An absolute line height in points.
127    Pt(f32),
128}
129impl LineHeightValue {
130    pub fn resolve(&self, font_size: f32) -> f32 {
131        match self {
132            LineHeightValue::Pt(value) => *value,
133            LineHeightValue::Normal(multiplier) => font_size * multiplier,
134        }
135    }
136}
137
138impl From<f32> for LineHeightValue {
139    fn from(value: f32) -> Self {
140        LineHeightValue::Normal(value)
141    }
142}
143
144impl From<f64> for LineHeightValue {
145    fn from(value: f64) -> Self {
146        LineHeightValue::Normal(value as f32)
147    }
148}
149
150impl From<i32> for LineHeightValue {
151    fn from(value: i32) -> Self {
152        LineHeightValue::Normal(value as f32)
153    }
154}
155
156/// Text styling attributes used to configure font properties, color, and layout.
157///
158/// `Attrs` uses a builder pattern where each setter consumes and returns `self`,
159/// making it easy to chain calls. Unset fields (`None`) inherit from the layout
160/// defaults when applied to a Parley builder.
161///
162/// # Defaults
163///
164/// | Property    | Default                           |
165/// |-------------|-----------------------------------|
166/// | font_size   | `16.0`                            |
167/// | line_height | `LineHeightValue::Normal(1.0)`    |
168/// | color       | `None` (inherits from context)    |
169/// | family      | `None` (system default)           |
170/// | weight      | `None` (normal)                   |
171/// | style       | `None` (normal)                   |
172/// | font_width  | `None` (normal)                   |
173/// | metadata    | `None`                            |
174///
175/// # Example
176///
177/// ```
178/// use floem_renderer::text::{Attrs, FamilyOwned, FontWeight, LineHeightValue};
179/// use peniko::Color;
180///
181/// let families = [FamilyOwned::Name("Inter".to_string()), FamilyOwned::SansSerif];
182/// let attrs = Attrs::new()
183///     .family(&families)
184///     .font_size(14.0)
185///     .weight(FontWeight::BOLD)
186///     .color(Color::WHITE)
187///     .line_height(LineHeightValue::Normal(1.4));
188/// ```
189#[derive(Clone, Debug)]
190pub struct Attrs<'a> {
191    /// Font size in pixels.
192    pub font_size: f32,
193    /// Line height mode — either a multiplier of `font_size` or an absolute point value.
194    line_height: LineHeightValue,
195    /// Text color, or `None` to inherit from the rendering context.
196    color: Option<Color>,
197    /// Ordered list of font families to try, or `None` to use the system default.
198    family: Option<&'a [FamilyOwned]>,
199    /// Font weight (e.g. normal, bold), or `None` for the default weight.
200    weight: Option<FontWeight>,
201    /// Font style (normal, italic, oblique), or `None` for normal.
202    style: Option<FontStyle>,
203    /// Font width / stretch (e.g. condensed, expanded), or `None` for normal.
204    font_width: Option<FontWidth>,
205    /// Word break strength used during wrapping, or `None` for Parley's default.
206    word_break: Option<WordBreakStrength>,
207    /// Application-defined metadata carried through layout without interpretation.
208    metadata: Option<usize>,
209}
210
211impl Default for Attrs<'_> {
212    fn default() -> Self {
213        Self::new()
214    }
215}
216
217impl<'a> Attrs<'a> {
218    /// Creates a new `Attrs` with default values (16px font, 1.0 line height multiplier).
219    pub fn new() -> Self {
220        Self {
221            font_size: 16.0,
222            line_height: LineHeightValue::Normal(1.0),
223            color: None,
224            family: None,
225            weight: None,
226            style: None,
227            font_width: None,
228            word_break: None,
229            metadata: None,
230        }
231    }
232
233    /// Sets the text color.
234    pub fn color(mut self, color: Color) -> Self {
235        self.color = Some(color);
236        self
237    }
238
239    /// Sets the font family list. Families are tried in order as fallbacks.
240    pub fn family(mut self, family: &'a [FamilyOwned]) -> Self {
241        self.family = Some(family);
242        self
243    }
244
245    /// Sets the font width (stretch), e.g. condensed or expanded.
246    pub fn font_width(mut self, stretch: FontWidth) -> Self {
247        self.font_width = Some(stretch);
248        self
249    }
250
251    /// Sets the font style (normal, italic, or oblique).
252    pub fn font_style(mut self, font_style: FontStyle) -> Self {
253        self.style = Some(font_style);
254        self
255    }
256
257    /// Sets the font weight (e.g. [`FontWeight::BOLD`]).
258    pub fn weight(mut self, weight: FontWeight) -> Self {
259        self.weight = Some(weight);
260        self
261    }
262
263    /// Sets the font weight from a raw numeric value (typically 100–900).
264    pub fn raw_weight(mut self, weight: u16) -> Self {
265        self.weight = Some(FontWeight::new(weight as f32));
266        self
267    }
268
269    /// Sets the font size in pixels.
270    pub fn font_size(mut self, font_size: f32) -> Self {
271        self.font_size = font_size;
272        self
273    }
274
275    /// Sets the word break strength used when text wrapping is enabled.
276    pub fn word_break(mut self, word_break: WordBreakStrength) -> Self {
277        self.word_break = Some(word_break);
278        self
279    }
280
281    /// Sets the line height. See [`LineHeightValue`] for the available modes.
282    pub fn line_height(mut self, line_height: LineHeightValue) -> Self {
283        self.line_height = line_height;
284        self
285    }
286
287    /// Sets an opaque metadata value that is carried through layout.
288    ///
289    /// This can be used to associate application-specific data (e.g. a span
290    /// identifier) with a range of text.
291    pub fn metadata(mut self, metadata: usize) -> Self {
292        self.metadata = Some(metadata);
293        self
294    }
295
296    /// Returns the text color, or `None` if unset.
297    pub fn get_color(&self) -> Option<Color> {
298        self.color
299    }
300
301    /// Returns the line height setting.
302    pub fn get_line_height(&self) -> LineHeightValue {
303        self.line_height
304    }
305
306    /// Returns the font family list, or `None` if unset.
307    pub fn get_family(&self) -> Option<&'a [FamilyOwned]> {
308        self.family
309    }
310
311    /// Returns the font weight, or `None` if unset.
312    pub fn get_weight(&self) -> Option<FontWeight> {
313        self.weight
314    }
315
316    /// Returns the font style, or `None` if unset.
317    pub fn get_font_style(&self) -> Option<FontStyle> {
318        self.style
319    }
320
321    /// Returns the font width (stretch), or `None` if unset.
322    pub fn get_stretch(&self) -> Option<FontWidth> {
323        self.font_width
324    }
325
326    /// Returns the word break strength, or `None` if unset.
327    pub fn get_word_break(&self) -> Option<WordBreakStrength> {
328        self.word_break
329    }
330
331    /// Returns the metadata value, or `None` if unset.
332    pub fn get_metadata(&self) -> Option<usize> {
333        self.metadata
334    }
335
336    /// Computes the effective line height in pixels.
337    ///
338    /// For [`LineHeightValue::Normal`], this multiplies the font size by the factor.
339    /// For [`LineHeightValue::Pt`], the point value is returned directly.
340    pub fn effective_line_height(&self) -> f32 {
341        match self.line_height {
342            LineHeightValue::Normal(n) => self.font_size * n,
343            LineHeightValue::Pt(n) => n,
344        }
345    }
346
347    /// Pushes all set properties as defaults onto a Parley [`RangedBuilder`].
348    ///
349    /// Font size and line height are always pushed. Optional properties (color,
350    /// family, weight, style, width) are only pushed when set.
351    ///
352    /// [`RangedBuilder`]: parley::RangedBuilder
353    pub fn apply_defaults(&self, builder: &mut parley::RangedBuilder<'_, TextBrush>) {
354        builder.push_default(StyleProperty::FontSize(self.font_size));
355        let lh = self.effective_line_height();
356        builder.push_default(StyleProperty::LineHeight(
357            parley::style::LineHeight::Absolute(lh),
358        ));
359        if let Some(color) = self.color {
360            builder.push_default(StyleProperty::Brush(TextBrush(color)));
361        }
362        if let Some(family) = self.family {
363            let stack = FamilyOwned::to_font_stack(family);
364            builder.push_default(StyleProperty::FontStack(stack));
365        }
366        if let Some(weight) = self.weight {
367            builder.push_default(StyleProperty::FontWeight(weight));
368        }
369        if let Some(style) = self.style {
370            builder.push_default(StyleProperty::FontStyle(style));
371        }
372        if let Some(width) = self.font_width {
373            builder.push_default(StyleProperty::FontWidth(width));
374        }
375        if let Some(word_break) = self.word_break {
376            builder.push_default(StyleProperty::WordBreak(word_break));
377        }
378    }
379
380    /// Pushes style properties for a specific byte range onto a Parley [`RangedBuilder`].
381    ///
382    /// Only properties that differ from `defaults` are pushed, avoiding redundant
383    /// work when a span shares most attributes with the base style.
384    ///
385    /// [`RangedBuilder`]: parley::RangedBuilder
386    pub fn apply_range(
387        &self,
388        builder: &mut parley::RangedBuilder<'_, TextBrush>,
389        range: Range<usize>,
390        defaults: &Attrs<'_>,
391    ) {
392        if self.font_size != defaults.font_size {
393            builder.push(StyleProperty::FontSize(self.font_size), range.clone());
394        }
395        if self.effective_line_height() != defaults.effective_line_height() {
396            let lh = self.effective_line_height();
397            builder.push(
398                StyleProperty::LineHeight(parley::style::LineHeight::Absolute(lh)),
399                range.clone(),
400            );
401        }
402        if let Some(color) = self.color {
403            builder.push(StyleProperty::Brush(TextBrush(color)), range.clone());
404        }
405        if let Some(family) = self.family {
406            let stack = FamilyOwned::to_font_stack(family);
407            builder.push(StyleProperty::FontStack(stack), range.clone());
408        }
409        if let Some(weight) = self.weight {
410            builder.push(StyleProperty::FontWeight(weight), range.clone());
411        }
412        if let Some(style) = self.style {
413            builder.push(StyleProperty::FontStyle(style), range.clone());
414        }
415        if let Some(width) = self.font_width {
416            builder.push(StyleProperty::FontWidth(width), range.clone());
417        }
418        if let Some(word_break) = self.word_break {
419            builder.push(StyleProperty::WordBreak(word_break), range);
420        }
421    }
422}
423
424/// An owned version of [`Attrs`] that does not borrow the font family slice.
425///
426/// This is used internally by [`AttrsList`] to store attribute spans, since spans
427/// need to own their data independently of the caller's lifetime.
428///
429/// # Example
430///
431/// ```
432/// use floem_renderer::text::{Attrs, AttrsOwned, FamilyOwned, FontWeight};
433///
434/// let families = [FamilyOwned::Monospace];
435/// let attrs = Attrs::new().family(&families).weight(FontWeight::BOLD);
436/// let owned = AttrsOwned::new(attrs);
437///
438/// // Convert back to a borrowed Attrs for use with builders.
439/// let borrowed = owned.as_attrs();
440/// ```
441#[derive(Clone, Debug)]
442pub struct AttrsOwned {
443    /// Font size in pixels.
444    pub font_size: f32,
445    /// Line height mode — either a multiplier of `font_size` or an absolute pixel value.
446    line_height: LineHeightValue,
447    /// Text color, or `None` to inherit from the rendering context.
448    color: Option<Color>,
449    /// Owned list of font families to try, or `None` to use the system default.
450    family: Option<Vec<FamilyOwned>>,
451    /// Font weight (e.g. normal, bold), or `None` for the default weight.
452    weight: Option<FontWeight>,
453    /// Font style (normal, italic, oblique), or `None` for normal.
454    style: Option<FontStyle>,
455    /// Font width / stretch (e.g. condensed, expanded), or `None` for normal.
456    font_width: Option<FontWidth>,
457    /// Word break strength used during wrapping, or `None` for Parley's default.
458    word_break: Option<WordBreakStrength>,
459    /// Application-defined metadata carried through layout without interpretation.
460    metadata: Option<usize>,
461}
462
463impl AttrsOwned {
464    /// Creates an owned copy of the given [`Attrs`], cloning the font family slice if present.
465    pub fn new(attrs: Attrs) -> Self {
466        Self {
467            font_size: attrs.font_size,
468            line_height: attrs.line_height,
469            color: attrs.color,
470            family: attrs.family.map(|f| f.to_vec()),
471            weight: attrs.weight,
472            style: attrs.style,
473            font_width: attrs.font_width,
474            word_break: attrs.word_break,
475            metadata: attrs.metadata,
476        }
477    }
478
479    /// Returns a borrowed [`Attrs`] referencing this owned data.
480    pub fn as_attrs(&self) -> Attrs<'_> {
481        Attrs {
482            font_size: self.font_size,
483            line_height: self.line_height,
484            color: self.color,
485            family: self.family.as_deref(),
486            weight: self.weight,
487            style: self.style,
488            font_width: self.font_width,
489            word_break: self.word_break,
490            metadata: self.metadata,
491        }
492    }
493}
494
495/// A list of text attributes with default styling and per-range overrides.
496///
497/// `AttrsList` pairs a set of default [`Attrs`] with zero or more byte-range spans
498/// that override specific properties. When applied to a Parley builder via
499/// [`apply_to_builder`](Self::apply_to_builder), the defaults are pushed first,
500/// then each span is layered on top for its range.
501///
502/// # Example
503///
504/// ```
505/// use floem_renderer::text::{Attrs, AttrsList, FontWeight};
506/// use peniko::Color;
507///
508/// let mut attrs_list = AttrsList::new(Attrs::new().font_size(14.0));
509///
510/// // Make bytes 0..5 bold and red.
511/// attrs_list.add_span(
512///     0..5,
513///     Attrs::new()
514///         .font_size(14.0)
515///         .weight(FontWeight::BOLD)
516///         .color(Color::WHITE),
517/// );
518/// ```
519#[derive(Clone, Debug)]
520pub struct AttrsList {
521    defaults: AttrsOwned,
522    spans: Vec<(Range<usize>, AttrsOwned)>,
523}
524
525impl PartialEq for AttrsList {
526    fn eq(&self, _other: &Self) -> bool {
527        // AttrsList comparison is expensive; use identity comparison or always false
528        false
529    }
530}
531
532impl AttrsList {
533    /// Creates a new attribute list with the given default attributes and no spans.
534    pub fn new(defaults: Attrs) -> Self {
535        Self {
536            defaults: AttrsOwned::new(defaults),
537            spans: Vec::new(),
538        }
539    }
540
541    /// Returns the default attributes.
542    pub fn defaults(&self) -> Attrs<'_> {
543        self.defaults.as_attrs()
544    }
545
546    /// Removes all attribute spans, keeping only the defaults.
547    pub fn clear_spans(&mut self) {
548        self.spans.clear();
549    }
550
551    /// Adds an attribute span for the given byte range.
552    ///
553    /// Any existing spans that overlap with `range` are removed before the new
554    /// span is inserted.
555    pub fn add_span(&mut self, range: Range<usize>, attrs: Attrs) {
556        // Remove any previous spans that overlap with this range
557        self.spans
558            .retain(|(r, _)| r.end <= range.start || r.start >= range.end);
559        self.spans.push((range, AttrsOwned::new(attrs)));
560    }
561
562    /// Returns the attributes at the given byte index.
563    ///
564    /// If a span covers `index`, its attributes are returned. Otherwise the
565    /// defaults are returned.
566    pub fn get_span(&self, index: usize) -> Attrs<'_> {
567        for (range, attrs) in &self.spans {
568            if range.contains(&index) {
569                return attrs.as_attrs();
570            }
571        }
572        self.defaults.as_attrs()
573    }
574
575    /// Splits this attribute list at the given byte index.
576    ///
577    /// Returns a new `AttrsList` covering `[index..)` with span ranges shifted
578    /// to start from zero. Spans that cross the split point are duplicated into
579    /// both halves with their ranges adjusted accordingly. `self` is left
580    /// containing only the `[..index)` portion.
581    pub fn split_off(&mut self, index: usize) -> Self {
582        let mut new_spans = Vec::new();
583        let mut remaining = Vec::new();
584
585        for (range, attrs) in self.spans.drain(..) {
586            if range.start >= index {
587                new_spans.push((range.start - index..range.end - index, attrs));
588            } else if range.end > index {
589                remaining.push((range.start..index, attrs.clone()));
590                new_spans.push((0..range.end - index, attrs));
591            } else {
592                remaining.push((range, attrs));
593            }
594        }
595
596        self.spans = remaining;
597
598        AttrsList {
599            defaults: self.defaults.clone(),
600            spans: new_spans,
601        }
602    }
603
604    /// Applies all defaults and spans to a Parley [`RangedBuilder`].
605    ///
606    /// This first pushes the default attributes, then layers each span on top
607    /// for its byte range. Span properties that match the defaults are skipped
608    /// to avoid redundant work.
609    ///
610    /// [`RangedBuilder`]: parley::RangedBuilder
611    pub fn apply_to_builder(&self, builder: &mut parley::RangedBuilder<'_, TextBrush>) {
612        let defaults = self.defaults.as_attrs();
613        defaults.apply_defaults(builder);
614        for (range, attrs) in &self.spans {
615            attrs
616                .as_attrs()
617                .apply_range(builder, range.clone(), &defaults);
618        }
619    }
620
621    /// Returns the inner spans as a slice of `(byte_range, attributes)` pairs.
622    pub fn spans(&self) -> &[(Range<usize>, AttrsOwned)] {
623        &self.spans
624    }
625}
626
627/// A streaming parser for CSS-style comma-separated font family lists.
628///
629/// Created by [`FamilyOwned::parse_list`]. Walks the input bytes left-to-right,
630/// yielding one [`FamilyOwned`] per entry. The parser handles:
631///
632/// - **Quoted names** (single `'` or double `"` quotes) — the content between
633///   the quotes is trimmed and returned as [`FamilyOwned::Name`].
634/// - **Unquoted generic keywords** (`serif`, `sans-serif`, `monospace`,
635///   `cursive`, `fantasy`) — matched case-insensitively and mapped to the
636///   corresponding enum variant.
637/// - **Unquoted custom names** — everything up to the next comma, trimmed.
638///
639/// Leading/trailing whitespace and commas between entries are skipped.
640#[derive(Clone)]
641struct ParseList<'a> {
642    /// The raw input bytes (must be valid UTF-8).
643    source: &'a [u8],
644    /// Cached `source.len()`.
645    len: usize,
646    /// Current read position in `source`.
647    pos: usize,
648}
649
650impl Iterator for ParseList<'_> {
651    type Item = FamilyOwned;
652
653    /// Advances the parser and returns the next [`FamilyOwned`], or `None`
654    /// when the input is exhausted.
655    fn next(&mut self) -> Option<Self::Item> {
656        let mut quote = None;
657        let mut pos = self.pos;
658        while pos < self.len && {
659            let ch = self.source[pos];
660            ch.is_ascii_whitespace() || ch == b','
661        } {
662            pos += 1;
663        }
664        self.pos = pos;
665        if pos >= self.len {
666            return None;
667        }
668        let first = self.source[pos];
669        let mut start = pos;
670        match first {
671            b'"' | b'\'' => {
672                quote = Some(first);
673                pos += 1;
674                start += 1;
675            }
676            _ => {}
677        }
678        if let Some(quote) = quote {
679            while pos < self.len {
680                if self.source[pos] == quote {
681                    self.pos = pos + 1;
682                    return Some(FamilyOwned::Name(
683                        core::str::from_utf8(self.source.get(start..pos)?)
684                            .ok()?
685                            .trim()
686                            .to_string(),
687                    ));
688                }
689                pos += 1;
690            }
691            self.pos = pos;
692            return Some(FamilyOwned::Name(
693                core::str::from_utf8(self.source.get(start..pos)?)
694                    .ok()?
695                    .trim()
696                    .to_string(),
697            ));
698        }
699        let mut end = start;
700        while pos < self.len {
701            if self.source[pos] == b',' {
702                pos += 1;
703                break;
704            }
705            pos += 1;
706            end += 1;
707        }
708        self.pos = pos;
709        let name = core::str::from_utf8(self.source.get(start..end)?)
710            .ok()?
711            .trim();
712        Some(match name.to_lowercase().as_str() {
713            "serif" => FamilyOwned::Serif,
714            "sans-serif" => FamilyOwned::SansSerif,
715            "monospace" => FamilyOwned::Monospace,
716            "cursive" => FamilyOwned::Cursive,
717            "fantasy" => FamilyOwned::Fantasy,
718            _ => FamilyOwned::Name(name.to_string()),
719        })
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    // ========================== FamilyOwned ==========================
728
729    #[test]
730    fn parse_list_named_and_generic() {
731        let families: Vec<_> = FamilyOwned::parse_list("Arial, sans-serif").collect();
732        assert_eq!(
733            families,
734            vec![
735                FamilyOwned::Name("Arial".to_string()),
736                FamilyOwned::SansSerif,
737            ]
738        );
739    }
740
741    #[test]
742    fn parse_list_quoted_names() {
743        let families: Vec<_> =
744            FamilyOwned::parse_list("'Fira Code', \"Noto Sans\", monospace").collect();
745        assert_eq!(
746            families,
747            vec![
748                FamilyOwned::Name("Fira Code".to_string()),
749                FamilyOwned::Name("Noto Sans".to_string()),
750                FamilyOwned::Monospace,
751            ]
752        );
753    }
754
755    #[test]
756    fn parse_list_all_generics() {
757        let families: Vec<_> =
758            FamilyOwned::parse_list("serif, sans-serif, monospace, cursive, fantasy").collect();
759        assert_eq!(
760            families,
761            vec![
762                FamilyOwned::Serif,
763                FamilyOwned::SansSerif,
764                FamilyOwned::Monospace,
765                FamilyOwned::Cursive,
766                FamilyOwned::Fantasy,
767            ]
768        );
769    }
770
771    #[test]
772    fn parse_list_case_insensitive() {
773        let families: Vec<_> = FamilyOwned::parse_list("SERIF, Sans-Serif").collect();
774        assert_eq!(families, vec![FamilyOwned::Serif, FamilyOwned::SansSerif]);
775    }
776
777    #[test]
778    fn parse_list_empty() {
779        let families: Vec<_> = FamilyOwned::parse_list("").collect();
780        assert!(families.is_empty());
781    }
782
783    #[test]
784    fn parse_list_whitespace_only() {
785        let families: Vec<_> = FamilyOwned::parse_list("  , , ").collect();
786        assert!(families.is_empty());
787    }
788
789    #[test]
790    fn to_font_stack_single_named() {
791        let families = vec![FamilyOwned::Name("Inter".to_string())];
792        let stack = FamilyOwned::to_font_stack(&families);
793        assert!(matches!(stack, FontStack::Source(_)));
794    }
795
796    #[test]
797    fn to_font_stack_single_generic() {
798        let families = vec![FamilyOwned::Monospace];
799        let stack = FamilyOwned::to_font_stack(&families);
800        assert!(matches!(stack, FontStack::Single(_)));
801    }
802
803    #[test]
804    fn to_font_stack_multi_family_uses_list() {
805        let families = vec![
806            FamilyOwned::Name("Inter".to_string()),
807            FamilyOwned::Monospace,
808            FamilyOwned::SansSerif,
809        ];
810        let stack = FamilyOwned::to_font_stack(&families);
811        match stack {
812            FontStack::List(list) => {
813                assert_eq!(list.len(), 3, "all families should be preserved");
814                assert!(matches!(list[0], FontFamily::Named(_)));
815                assert!(matches!(
816                    list[1],
817                    FontFamily::Generic(GenericFamily::Monospace)
818                ));
819                assert!(matches!(
820                    list[2],
821                    FontFamily::Generic(GenericFamily::SansSerif)
822                ));
823            }
824            other => panic!("expected FontStack::List, got {other:?}"),
825        }
826    }
827
828    #[test]
829    fn to_font_stack_two_named_families() {
830        let families = vec![
831            FamilyOwned::Name("Fira Code".to_string()),
832            FamilyOwned::Name("Cascadia Code".to_string()),
833        ];
834        let stack = FamilyOwned::to_font_stack(&families);
835        assert!(
836            matches!(stack, FontStack::List(_)),
837            "two families should produce List"
838        );
839    }
840
841    #[test]
842    fn to_font_stack_empty_defaults_to_sans_serif() {
843        let families: Vec<FamilyOwned> = vec![];
844        let stack = FamilyOwned::to_font_stack(&families);
845        assert!(matches!(
846            stack,
847            FontStack::Single(FontFamily::Generic(GenericFamily::SansSerif))
848        ));
849    }
850
851    // ========================== Attrs ==========================
852
853    #[test]
854    fn attrs_defaults() {
855        let a = Attrs::new();
856        assert_eq!(a.font_size, 16.0);
857        assert_eq!(a.get_line_height(), LineHeightValue::Normal(1.0));
858        assert_eq!(a.get_color(), None);
859        assert_eq!(a.get_family(), None);
860        assert_eq!(a.get_weight(), None);
861        assert_eq!(a.get_font_style(), None);
862        assert_eq!(a.get_stretch(), None);
863        assert_eq!(a.get_metadata(), None);
864    }
865
866    #[test]
867    fn attrs_builder_chain() {
868        let families = [FamilyOwned::Monospace];
869        let a = Attrs::new()
870            .font_size(20.0)
871            .color(Color::WHITE)
872            .family(&families)
873            .weight(FontWeight::BOLD)
874            .font_style(FontStyle::Italic)
875            .font_width(FontWidth::CONDENSED)
876            .line_height(LineHeightValue::Pt(24.0))
877            .metadata(42);
878
879        assert_eq!(a.font_size, 20.0);
880        assert_eq!(a.get_color(), Some(Color::WHITE));
881        assert_eq!(a.get_family(), Some(families.as_slice()));
882        assert_eq!(a.get_weight(), Some(FontWeight::BOLD));
883        assert_eq!(a.get_font_style(), Some(FontStyle::Italic));
884        assert_eq!(a.get_stretch(), Some(FontWidth::CONDENSED));
885        assert_eq!(a.get_line_height(), LineHeightValue::Pt(24.0));
886        assert_eq!(a.get_metadata(), Some(42));
887    }
888
889    #[test]
890    fn attrs_raw_weight() {
891        let a = Attrs::new().raw_weight(700);
892        assert_eq!(a.get_weight(), Some(FontWeight::new(700.0)));
893    }
894
895    #[test]
896    fn effective_line_height_normal_multiplier() {
897        let a = Attrs::new()
898            .font_size(20.0)
899            .line_height(LineHeightValue::Normal(1.5));
900        assert!((a.effective_line_height() - 30.0).abs() < f32::EPSILON);
901    }
902
903    #[test]
904    fn effective_line_height_px_absolute() {
905        let a = Attrs::new()
906            .font_size(20.0)
907            .line_height(LineHeightValue::Pt(24.0));
908        assert!((a.effective_line_height() - 24.0).abs() < f32::EPSILON);
909    }
910
911    #[test]
912    fn effective_line_height_default() {
913        // Default: Normal(1.0), font_size 16.0 → 16.0.
914        let a = Attrs::new();
915        assert!((a.effective_line_height() - 16.0).abs() < f32::EPSILON);
916    }
917
918    // ========================== AttrsOwned ==========================
919
920    #[test]
921    fn attrs_owned_roundtrip() {
922        let families = [
923            FamilyOwned::Name("Inter".to_string()),
924            FamilyOwned::SansSerif,
925        ];
926        let a = Attrs::new()
927            .font_size(18.0)
928            .family(&families)
929            .weight(FontWeight::BOLD)
930            .color(Color::WHITE)
931            .metadata(7);
932
933        let owned = AttrsOwned::new(a);
934        let back = owned.as_attrs();
935
936        assert_eq!(back.font_size, 18.0);
937        assert_eq!(back.get_weight(), Some(FontWeight::BOLD));
938        assert_eq!(back.get_color(), Some(Color::WHITE));
939        assert_eq!(back.get_metadata(), Some(7));
940        // Family should be preserved.
941        let fam = back.get_family().unwrap();
942        assert_eq!(fam.len(), 2);
943        assert_eq!(fam[0], FamilyOwned::Name("Inter".to_string()));
944        assert_eq!(fam[1], FamilyOwned::SansSerif);
945    }
946
947    #[test]
948    fn attrs_owned_no_family() {
949        let a = Attrs::new();
950        let owned = AttrsOwned::new(a);
951        assert_eq!(owned.as_attrs().get_family(), None);
952    }
953
954    // ========================== AttrsList ==========================
955
956    #[test]
957    fn attrs_list_new_has_no_spans() {
958        let list = AttrsList::new(Attrs::new().font_size(14.0));
959        assert_eq!(list.defaults().font_size, 14.0);
960        assert!(list.spans().is_empty());
961    }
962
963    #[test]
964    fn attrs_list_add_span_and_get() {
965        let mut list = AttrsList::new(Attrs::new().font_size(14.0));
966        list.add_span(5..10, Attrs::new().font_size(20.0).weight(FontWeight::BOLD));
967
968        // Inside span.
969        let at7 = list.get_span(7);
970        assert_eq!(at7.font_size, 20.0);
971        assert_eq!(at7.get_weight(), Some(FontWeight::BOLD));
972
973        // Outside span — returns defaults.
974        let at0 = list.get_span(0);
975        assert_eq!(at0.font_size, 14.0);
976        assert_eq!(at0.get_weight(), None);
977
978        // At span boundary (end is exclusive).
979        let at10 = list.get_span(10);
980        assert_eq!(at10.font_size, 14.0);
981    }
982
983    #[test]
984    fn attrs_list_overlapping_span_replaces() {
985        let mut list = AttrsList::new(Attrs::new());
986        list.add_span(0..10, Attrs::new().font_size(20.0));
987        list.add_span(5..15, Attrs::new().font_size(30.0));
988
989        // Original span 0..10 should be removed since it overlaps 5..15.
990        assert_eq!(list.spans().len(), 1);
991        assert_eq!(list.spans()[0].0, 5..15);
992    }
993
994    #[test]
995    fn attrs_list_clear_spans() {
996        let mut list = AttrsList::new(Attrs::new());
997        list.add_span(0..5, Attrs::new().font_size(20.0));
998        list.add_span(5..10, Attrs::new().font_size(30.0));
999        assert_eq!(list.spans().len(), 2);
1000
1001        list.clear_spans();
1002        assert!(list.spans().is_empty());
1003        // Defaults preserved.
1004        assert_eq!(list.defaults().font_size, 16.0);
1005    }
1006
1007    #[test]
1008    fn attrs_list_split_off_basic() {
1009        let mut list = AttrsList::new(Attrs::new().font_size(14.0));
1010        list.add_span(2..4, Attrs::new().font_size(20.0));
1011        list.add_span(6..8, Attrs::new().font_size(30.0));
1012
1013        let right = list.split_off(5);
1014
1015        // Left: only 2..4 remains (entirely before split point).
1016        assert_eq!(list.spans().len(), 1);
1017        assert_eq!(list.spans()[0].0, 2..4);
1018
1019        // Right: 6..8 shifted to 1..3.
1020        assert_eq!(right.spans().len(), 1);
1021        assert_eq!(right.spans()[0].0, 1..3);
1022    }
1023
1024    #[test]
1025    fn attrs_list_split_off_crossing_span() {
1026        let mut list = AttrsList::new(Attrs::new());
1027        list.add_span(3..7, Attrs::new().font_size(20.0));
1028
1029        let right = list.split_off(5);
1030
1031        // Left: 3..5 (truncated at split).
1032        assert_eq!(list.spans().len(), 1);
1033        assert_eq!(list.spans()[0].0, 3..5);
1034
1035        // Right: 0..2 (crossing span starts at 0 in new list).
1036        assert_eq!(right.spans().len(), 1);
1037        assert_eq!(right.spans()[0].0, 0..2);
1038        assert_eq!(right.spans()[0].1.font_size, 20.0);
1039    }
1040
1041    #[test]
1042    fn attrs_list_split_off_empty() {
1043        let mut list = AttrsList::new(Attrs::new().font_size(14.0));
1044        let right = list.split_off(0);
1045        assert!(list.spans().is_empty());
1046        assert!(right.spans().is_empty());
1047        assert_eq!(right.defaults().font_size, 14.0);
1048    }
1049}