Skip to main content

cotis_defaults/element_configs/
text_config.rs

1//! Text element configuration types.
2//!
3//! The text/font/measuring stack is planned for refactoring in future versions.
4//! Treat these APIs as functional but evolving.
5use crate::colors::Color;
6#[cfg(feature = "complex_colored_text")]
7use crate::colors::ColorLayer;
8use cotis::utils::OwnedOrRef;
9use cotis::utils::OwnedOrRef::Ref;
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12use std::ops::Range;
13
14#[derive(Debug, Clone, Copy)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct TextConfig {
17    /// Cotis does not manage font identity automatically.
18    ///
19    /// Assign a stable ID per loaded font and provide it through this field.
20    pub font_id: u16,
21    /// The font size of the text.
22    pub font_size: f32,
23    /// The spacing between letters.
24    pub letter_spacing: f32,
25    /// The height of each line of text.
26    ///
27    /// A value of `0.0` is treated by some renderers as "use backend default".
28    pub line_height: f32,
29    /// Defines the text wrapping behavior.
30    pub wrap_mode: TextElementConfigWrapMode,
31    /// The alignment of the text.
32    pub alignment: TextAlignment,
33}
34
35#[derive(Debug, Clone)]
36#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
37#[cfg_attr(not(feature = "complex_colored_text"), derive(Copy))]
38pub struct TextStyle {
39    /// The color of the text.
40    #[cfg(not(feature = "complex_colored_text"))]
41    pub color: Color,
42    #[cfg(feature = "complex_colored_text")]
43    pub color: ColorLayer,
44}
45
46impl Default for TextConfig {
47    /// Creates a neutral text config (font id 0, zero sizing values, word wrapping).
48    fn default() -> Self {
49        Self {
50            font_id: 0,
51            font_size: 0.,
52            letter_spacing: 0.,
53            line_height: 0.,
54            wrap_mode: TextElementConfigWrapMode::Words,
55            alignment: TextAlignment::Left,
56        }
57    }
58}
59
60impl Default for TextStyle {
61    /// Creates the default text style (solid black).
62    fn default() -> Self {
63        Self {
64            #[cfg(not(feature = "complex_colored_text"))]
65            color: Color::rgb(0.0, 0.0, 0.0),
66            #[cfg(feature = "complex_colored_text")]
67            color: ColorLayer::Solid(Color::rgb(0.0, 0.0, 0.0)),
68        }
69    }
70}
71
72#[derive(Debug, Clone, Copy)]
73#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
74pub enum TextElementConfigWrapMode {
75    /// Wraps on whitespaces without breaking words.
76    Words,
77    /// Only wraps on newline characters.
78    Newline,
79    /// Never wraps and can overflow from parent layout.
80    None,
81}
82
83#[derive(Debug, Clone, Copy)]
84#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
85pub enum TextAlignment {
86    /// Aligns the text to the left.
87    Left,
88    /// Aligns the text to the center.
89    Center,
90    /// Aligns the text to the right.
91    Right,
92}
93
94/// Leaf text element configuration.
95#[derive(Debug, Clone)]
96#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
97pub struct TextElementConfig<'a> {
98    /// Text content.
99    pub text: OwnedOrRef<'a, str>,
100    /// Text metrics and wrapping config.
101    pub config: TextConfig,
102    /// Text paint/style settings.
103    pub style: TextStyle,
104}
105
106/// Pair of text metrics config and style.
107#[derive(Debug, Clone)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109#[cfg_attr(not(feature = "complex_colored_text"), derive(Copy, Default))]
110pub struct TextConfigStylePair {
111    /// Text metrics config.
112    pub config: TextConfig,
113    /// Text style/color.
114    pub style: TextStyle,
115}
116
117impl<'a> Default for TextElementConfig<'a> {
118    fn default() -> Self {
119        Self {
120            text: Ref(""),
121            config: TextConfig::default(),
122            style: Default::default(),
123        }
124    }
125}
126
127/// Rich text configuration with per-range overrides.
128///
129/// `config_ranges` contains `(range, config_index)` pairs mapping text ranges to
130/// entries in [`configs`](Self::configs).
131#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
132pub struct RichTextElementConfig<'a> {
133    /// Full text content.
134    pub text: OwnedOrRef<'a, str>,
135    /// Fallback style/config used when no range override matches.
136    pub default_config: TextConfigStylePair,
137    /// Override configs used by range mappings.
138    pub configs: Vec<TextConfigStylePair>,
139    /// Mappings from character range to an index in `configs`.
140    pub config_ranges: Vec<(Range<usize>, usize)>,
141}
142
143/// Builder for [`RichTextElementConfig`].
144pub struct RichTextElementBuilder<'a> {
145    text: OwnedOrRef<'a, str>,
146    default_config: TextConfigStylePair,
147    configs: Vec<TextConfigStylePair>,
148    config_ranges: Vec<(Range<usize>, usize)>,
149}
150
151impl<'a> RichTextElementBuilder<'a> {
152    /// Creates a new builder with the provided text and default style/config.
153    pub fn new(text: OwnedOrRef<'a, str>, default_config: TextConfigStylePair) -> Self {
154        Self {
155            text,
156            default_config,
157            configs: vec![],
158            config_ranges: vec![],
159        }
160    }
161
162    /// Adds an override config and returns an index intended for `configure_range`.
163    pub fn add_config(&mut self, config: TextConfigStylePair) -> usize {
164        self.configs.push(config);
165        self.config_ranges.len() - 1
166    }
167
168    /// Applies an override config to a text range.
169    ///
170    /// # Panics
171    ///
172    /// Panics if `range.end` is outside the text length or `config_index` is not
173    /// valid for the current override set.
174    pub fn configure_range(&mut self, range: Range<usize>, config_index: usize) {
175        assert!(range.end < self.text.as_ref().len());
176        assert!(config_index <= self.config_ranges.len());
177        self.config_ranges.push((range, config_index));
178    }
179
180    /// Finalizes the builder into a [`RichTextElementConfig`].
181    pub fn into_config(self) -> RichTextElementConfig<'a> {
182        RichTextElementConfig {
183            text: self.text,
184            default_config: self.default_config,
185            configs: self.configs,
186            config_ranges: self.config_ranges,
187        }
188    }
189}