Skip to main content

telar_renderer_core/style/
mod.rs

1mod gradient;
2mod paint;
3mod scale;
4mod shape;
5
6pub use gradient::{Gradient, GradientKind, GradientStop, GradientStops};
7pub use paint::{FillRule, LineCap, LineJoin, Paint, Shadow, Stroke};
8pub use shape::{PathStyle, RectStyle, ShapeStyle};
9
10/// Horizontal alignment of text within its box. `Start` is the writing-direction start (left in LTR).
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum TextAlign {
13    #[default]
14    Start,
15    Center,
16    End,
17    Justify,
18}
19
20/// Which grid the glyphs are rasterized onto.
21///
22/// An axis of the style, like weight or slant — not a mode the whole renderer enters. Shaping,
23/// wrapping, bidi and the font stack are the same either way; only where a glyph lands and how its
24/// coverage is resolved change.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26pub enum GlyphRaster {
27    /// Subpixel origins and blended coverage: the sharpest text a screen can show at UI sizes.
28    #[default]
29    Smooth,
30    /// Whole-pixel origins and coverage resolved to on or off.
31    ///
32    /// What a font drawn on a pixel grid needs: a glyph shared between two columns, or an edge left
33    /// half-lit, is the grid the artist drew being taken apart. With a face designed at the size it is
34    /// used, this reproduces a bitmap font's output without Telar growing a second font format —
35    /// cosmic-text still shapes, wraps and falls back exactly as before.
36    Pixel,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct TextStyle {
41    pub font_size: f32,
42    pub paint: Paint,
43    pub shadow: Option<Shadow>,
44    /// OpenType weight axis: 400 is normal, 700 is bold. Selects the matching font face.
45    pub weight: u16,
46    pub italic: bool,
47    pub align: TextAlign,
48    /// Clamp the text to at most this many lines (`None` = unlimited). Lines beyond it are dropped.
49    pub max_lines: Option<u16>,
50    /// When clamped by `max_lines`, replace the overflowing tail with an ellipsis (`…`).
51    pub ellipsis: bool,
52    /// Line height as a multiple of `font_size` (e.g. `1.5`). `None` keeps the shaper's natural line height, so the default renders byte-for-byte as before.
53    pub line_height: Option<f32>,
54    /// Extra advance in logical pixels added after each glyph. `0.0` uses the font's natural advances.
55    pub letter_spacing: f32,
56    /// Which grid the glyphs land on. See [`GlyphRaster`].
57    pub raster: GlyphRaster,
58}
59
60impl TextStyle {
61    pub fn new(font_size: f32, paint: impl Into<Paint>) -> Self {
62        Self {
63            font_size,
64            paint: paint.into(),
65            shadow: None,
66            weight: 400,
67            italic: false,
68            align: TextAlign::Start,
69            max_lines: None,
70            ellipsis: false,
71            line_height: None,
72            letter_spacing: 0.0,
73            raster: GlyphRaster::Smooth,
74        }
75    }
76
77    pub fn with_weight(mut self, weight: u16) -> Self {
78        self.weight = weight;
79        self
80    }
81
82    /// Overrides the size a style was built at, so a style carrying theme-resolved weight and slant can be
83    /// re-sized without being rebuilt from scratch (and losing them).
84    pub fn with_size(mut self, font_size: f32) -> Self {
85        self.font_size = font_size;
86        self
87    }
88
89    /// Drops a shadow behind the glyphs — what keeps text legible over an image the style knows nothing about.
90    pub fn with_shadow(mut self, shadow: Shadow) -> Self {
91        self.shadow = Some(shadow);
92        self
93    }
94
95    pub fn with_italic(mut self, italic: bool) -> Self {
96        self.italic = italic;
97        self
98    }
99
100    pub fn with_align(mut self, align: TextAlign) -> Self {
101        self.align = align;
102        self
103    }
104
105    pub fn with_max_lines(mut self, max_lines: u16) -> Self {
106        self.max_lines = Some(max_lines);
107        self
108    }
109
110    pub fn with_ellipsis(mut self, ellipsis: bool) -> Self {
111        self.ellipsis = ellipsis;
112        self
113    }
114
115    pub fn with_line_height(mut self, line_height: f32) -> Self {
116        self.line_height = Some(line_height);
117        self
118    }
119
120    pub fn with_letter_spacing(mut self, letter_spacing: f32) -> Self {
121        self.letter_spacing = letter_spacing;
122        self
123    }
124
125    /// Puts the glyphs on whole pixels with coverage resolved to on or off. See [`GlyphRaster`].
126    pub fn with_raster(mut self, raster: GlyphRaster) -> Self {
127        self.raster = raster;
128        self
129    }
130}
131
132pub trait Scale: Sized {
133    fn scale(self, sf: f32) -> Self;
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::Color;
140
141    #[test]
142    fn text_style_new_stores_font_size() {
143        let style = TextStyle::new(16.0, Color::BLACK);
144        assert_eq!(style.font_size, 16.0);
145    }
146
147    #[test]
148    fn text_style_new_stores_color() {
149        let style = TextStyle::new(12.0, Color::WHITE);
150        assert_eq!(style.paint, Paint::Solid(Color::WHITE));
151    }
152
153    #[test]
154    fn text_style_defaults_to_natural_spacing() {
155        let style = TextStyle::new(16.0, Color::BLACK);
156        assert_eq!(style.line_height, None);
157        assert_eq!(style.letter_spacing, 0.0);
158    }
159
160    #[test]
161    fn text_style_builders_set_spacing() {
162        let style = TextStyle::new(16.0, Color::BLACK)
163            .with_line_height(1.5)
164            .with_letter_spacing(2.0);
165        assert_eq!(style.line_height, Some(1.5));
166        assert_eq!(style.letter_spacing, 2.0);
167    }
168}