Skip to main content

zenthra_text/types/
options.rs

1use crate::types::color::Color;
2use cosmic_text::{Align, Attrs, Buffer, FontSystem, Metrics};
3
4/// Options for configuring text rendering.
5/// Use the builder-like methods for a fluent API.
6#[derive(Debug, Clone, PartialEq)]
7pub struct TextOptions {
8    /// X coordinate of the text.
9    pub x: f32,
10    pub y: f32,
11
12    pub font_size: f32,
13    pub color: Color,
14    pub font_family: Option<String>,
15    pub font_weight: FontWeight,
16    pub font_style: FontStyle,
17
18    pub highlight_color: Option<Color>,
19
20    pub max_width: Option<f32>,
21    pub max_height: Option<f32>,
22    pub line_height: f32,
23    pub wrap: TextWrap,
24    /// Horizontal alignment. If None, per-line settings are preserved.
25    pub align: Option<HorizontalAlignment>,
26    /// Vertical alignment within the available space.
27    pub valign: Option<VerticalAlignment>,
28    pub min_width: Option<f32>,
29    pub clip_rect: Option<[f32; 4]>,
30    pub scale_factor: f32,
31    pub ellipsis: bool,
32}
33
34
35
36use std::cell::RefCell;
37thread_local! {
38    static LAST_APPLIED_OPTIONS: RefCell<TextOptions> = RefCell::new(TextOptions::default());
39}
40
41pub fn get_last_applied() -> TextOptions {
42    LAST_APPLIED_OPTIONS.with(|opts| opts.borrow().clone())
43}
44
45impl Default for TextOptions {
46    fn default() -> Self {
47        Self {
48            x: 0.0,
49            y: 0.0,
50            font_size: 16.0,
51            color: Color::WHITE,
52            font_family: None,
53            font_weight: FontWeight::Regular,
54            font_style: FontStyle::Normal,
55            highlight_color: None,
56            max_width: None,
57            max_height: None,
58            line_height: 1.2,
59            wrap: TextWrap::None,
60            align: None,
61            valign: None,
62            min_width: None,
63            clip_rect: None,
64            scale_factor: 1.0,
65            ellipsis: true,
66        }
67    }
68}
69
70
71impl TextOptions {
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    pub fn min_width(mut self, w: f32) -> Self {
77        self.min_width = Some(w);
78        self
79    }
80
81    pub fn at(mut self, x: f32, y: f32) -> Self {
82        self.x = x;
83        self.y = y;
84        self
85    }
86
87    pub fn font_size(mut self, size: f32) -> Self {
88        self.font_size = size;
89        self
90    }
91
92    pub fn color(mut self, color: Color) -> Self {
93        self.color = color;
94        self
95    }
96
97    pub fn font_family(mut self, family: impl Into<String>) -> Self {
98        self.font_family = Some(family.into());
99        self
100    }
101
102    pub fn font_weight(mut self, weight: FontWeight) -> Self {
103        self.font_weight = weight;
104        self
105    }
106
107    pub fn font_style(mut self, style: FontStyle) -> Self {
108        self.font_style = style;
109        self
110    }
111
112    pub fn highlight(mut self, color: Color) -> Self {
113        self.highlight_color = Some(color);
114        self
115    }
116
117
118    pub fn max_width(mut self, width: f32) -> Self {
119        self.max_width = Some(width);
120        self
121    }
122
123    /// Sets the maximum height for word wrapping and vertical alignment.
124    pub fn max_height(mut self, height: f32) -> Self {
125        self.max_height = Some(height);
126        self
127    }
128
129    pub fn line_height(mut self, height: f32) -> Self {
130        self.line_height = height;
131        self
132    }
133
134    pub fn wrap(mut self, strategy: TextWrap) -> Self {
135        self.wrap = strategy;
136        self
137    }
138
139    pub fn align(mut self, alignment: HorizontalAlignment) -> Self {
140        self.align = Some(alignment);
141        self
142    }
143
144    pub fn valign(mut self, alignment: VerticalAlignment) -> Self {
145        self.valign = Some(alignment);
146        self
147    }
148
149    pub fn clip_rect(mut self, x: f32, y: f32, w: f32, h: f32) -> Self {
150        self.clip_rect = Some([x, y, w, h]);
151        self
152    }
153
154    pub fn scale_factor(mut self, sf: f32) -> Self {
155        self.scale_factor = sf;
156        self
157    }
158
159    pub fn ellipsis(mut self, enabled: bool) -> Self {
160        self.ellipsis = enabled;
161        self
162    }
163
164
165    pub fn apply(&self, font_system: &mut FontSystem, buffer: &mut Buffer) {
166
167        // Save to global state for testing tools
168        LAST_APPLIED_OPTIONS.with(|opts| {
169            *opts.borrow_mut() = self.clone();
170        });
171
172        // Set metrics (font size and line height)
173        buffer.set_metrics(
174            font_system,
175            Metrics::new(self.font_size, self.font_size * self.line_height),
176        );
177
178        // Set size and wrap mode
179        let wrap = match self.wrap {
180            TextWrap::Word => cosmic_text::Wrap::Word,
181            TextWrap::Character => cosmic_text::Wrap::Glyph,
182            TextWrap::None => cosmic_text::Wrap::None,
183        };
184
185        
186        buffer.set_size(font_system, self.max_width, None);
187        buffer.set_wrap(font_system, wrap);
188
189        // Shape first, THEN apply alignment once glyphs are measured
190        buffer.shape_until_scroll(font_system, false);
191
192        if let Some(alignment) = self.align {
193            let align: Align = alignment.into();
194            for line in buffer.lines.iter_mut() {
195                line.set_align(Some(align));
196            }
197            // Re-shape once more to reflect alignment positions
198            buffer.shape_until_scroll(font_system, false);
199        }
200    }
201
202    pub fn as_attrs(&self) -> Attrs<'_> {
203        use crate::types::color::ColorExt;
204        let mut attrs = Attrs::new()
205            .color(self.color.to_cosmic())
206            .weight(self.font_weight.into())
207            .style(self.font_style.into());
208
209        if let Some(ref family) = self.font_family {
210            let family_enum = match family.to_lowercase().as_str() {
211                "sans-serif" => cosmic_text::Family::SansSerif,
212                "serif" => cosmic_text::Family::Serif,
213                "monospace" => cosmic_text::Family::Monospace,
214                _ => cosmic_text::Family::Name(family),
215            };
216            attrs = attrs.family(family_enum);
217        }
218
219        attrs
220    }
221}
222
223/// Supported font weights.
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
225pub enum FontWeight {
226    Thin,
227    ExtraLight,
228    Light,
229    #[default]
230    Regular,
231    Medium,
232    SemiBold,
233    Bold,
234    ExtraBold,
235    Black,
236    Custom(u16),
237}
238
239/// Supported font styles.
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
241pub enum FontStyle {
242    #[default]
243    Normal,
244    Italic,
245    Oblique,
246}
247
248impl From<FontStyle> for cosmic_text::Style {
249    fn from(style: FontStyle) -> Self {
250        match style {
251            FontStyle::Normal => cosmic_text::Style::Normal,
252            FontStyle::Italic => cosmic_text::Style::Italic,
253            FontStyle::Oblique => cosmic_text::Style::Oblique,
254        }
255    }
256}
257
258impl From<FontWeight> for cosmic_text::Weight {
259    fn from(weight: FontWeight) -> Self {
260        match weight {
261            FontWeight::Thin => cosmic_text::Weight::THIN,
262            FontWeight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
263            FontWeight::Light => cosmic_text::Weight::LIGHT,
264            FontWeight::Regular => cosmic_text::Weight::NORMAL,
265            FontWeight::Medium => cosmic_text::Weight::MEDIUM,
266            FontWeight::SemiBold => cosmic_text::Weight::SEMIBOLD,
267            FontWeight::Bold => cosmic_text::Weight::BOLD,
268            FontWeight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
269            FontWeight::Black => cosmic_text::Weight::BLACK,
270            FontWeight::Custom(w) => cosmic_text::Weight(w),
271        }
272    }
273}
274
275impl From<u16> for FontWeight {
276    fn from(w: u16) -> Self {
277        FontWeight::Custom(w)
278    }
279}
280
281impl From<f32> for FontWeight {
282    fn from(w: f32) -> Self {
283        FontWeight::Custom(w as u16)
284    }
285}
286
287/// Strategy for wrapping text when it exceeds `max_width`.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
289pub enum TextWrap {
290    #[default]
291    Word,
292    Character,
293    None,
294}
295
296impl From<bool> for TextWrap {
297    fn from(b: bool) -> Self {
298        if b { TextWrap::Word } else { TextWrap::None }
299    }
300}
301
302/// Horizontal text alignment.
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
304pub enum HorizontalAlignment {
305    #[default]
306    Left,
307    Center,
308    Right,
309    Justified,
310}
311
312/// Vertical text alignment.
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
314pub enum VerticalAlignment {
315    #[default]
316    Top,
317    Center,
318    Bottom,
319}
320
321/// Re-export EdgeInsets as Padding for Zentype compatibility
322pub use zenthra_core::EdgeInsets as Padding;
323
324impl From<HorizontalAlignment> for Align {
325    fn from(alignment: HorizontalAlignment) -> Self {
326        match alignment {
327            HorizontalAlignment::Left => Align::Left,
328            HorizontalAlignment::Center => Align::Center,
329            HorizontalAlignment::Right => Align::Right,
330            HorizontalAlignment::Justified => Align::Justified,
331        }
332    }
333}