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::{BorderWidths, PathStyle, RectStyle, ShapeStyle, border_inner_shape};
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    /// Keep the text on one line whatever width it is given, instead of wrapping into the box.
59    ///
60    /// What a label in a toolbar, a status bar or a table cell wants: it is a *name*, and a name broken
61    /// across two lines to fit a column reads as two things. Wrapping is the right default for prose and the
62    /// wrong one for everything that is really a token, so it is a property of the style rather than of the
63    /// box — the same text is a label in one place and a paragraph in another.
64    pub no_wrap: bool,
65}
66
67impl TextStyle {
68    pub fn new(font_size: f32, paint: impl Into<Paint>) -> Self {
69        Self {
70            font_size,
71            paint: paint.into(),
72            shadow: None,
73            weight: 400,
74            italic: false,
75            align: TextAlign::Start,
76            max_lines: None,
77            ellipsis: false,
78            line_height: None,
79            letter_spacing: 0.0,
80            raster: GlyphRaster::Smooth,
81            no_wrap: false,
82        }
83    }
84
85    /// Keeps the text on one line; see [`no_wrap`](Self::no_wrap).
86    pub fn with_no_wrap(mut self, no_wrap: bool) -> Self {
87        self.no_wrap = no_wrap;
88        self
89    }
90
91    pub fn with_weight(mut self, weight: u16) -> Self {
92        self.weight = weight;
93        self
94    }
95
96    /// Drops a shadow behind the glyphs — what keeps text legible over an image the style knows nothing about.
97    pub fn with_shadow(mut self, shadow: Shadow) -> Self {
98        self.shadow = Some(shadow);
99        self
100    }
101
102    pub fn with_italic(mut self, italic: bool) -> Self {
103        self.italic = italic;
104        self
105    }
106
107    pub fn with_align(mut self, align: TextAlign) -> Self {
108        self.align = align;
109        self
110    }
111
112    pub fn with_max_lines(mut self, max_lines: u16) -> Self {
113        self.max_lines = Some(max_lines);
114        self
115    }
116
117    pub fn with_ellipsis(mut self, ellipsis: bool) -> Self {
118        self.ellipsis = ellipsis;
119        self
120    }
121
122    pub fn with_line_height(mut self, line_height: f32) -> Self {
123        self.line_height = Some(line_height);
124        self
125    }
126
127    pub fn with_letter_spacing(mut self, letter_spacing: f32) -> Self {
128        self.letter_spacing = letter_spacing;
129        self
130    }
131
132    /// Puts the glyphs on whole pixels with coverage resolved to on or off. See [`GlyphRaster`].
133    pub fn with_raster(mut self, raster: GlyphRaster) -> Self {
134        self.raster = raster;
135        self
136    }
137}
138
139pub trait Scale: Sized {
140    fn scale(self, sf: f32) -> Self;
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::Color;
147
148    #[test]
149    fn text_style_new_stores_font_size() {
150        let style = TextStyle::new(16.0, Color::BLACK);
151        assert_eq!(style.font_size, 16.0);
152    }
153
154    #[test]
155    fn text_style_new_stores_color() {
156        let style = TextStyle::new(12.0, Color::WHITE);
157        assert_eq!(style.paint, Paint::Solid(Color::WHITE));
158    }
159
160    #[test]
161    fn text_style_defaults_to_natural_spacing() {
162        let style = TextStyle::new(16.0, Color::BLACK);
163        assert_eq!(style.line_height, None);
164        assert_eq!(style.letter_spacing, 0.0);
165    }
166
167    #[test]
168    fn text_style_builders_set_spacing() {
169        let style = TextStyle::new(16.0, Color::BLACK)
170            .with_line_height(1.5)
171            .with_letter_spacing(2.0);
172        assert_eq!(style.line_height, Some(1.5));
173        assert_eq!(style.letter_spacing, 2.0);
174    }
175}