maycoon_widgets/
text.rs

1use crate::ext::WidgetLayoutExt;
2use maycoon_core::app::info::AppInfo;
3use maycoon_core::app::update::Update;
4use maycoon_core::layout::{Dimension, LayoutNode, LayoutStyle, StyleNode};
5use maycoon_core::skrifa::instance::Size;
6use maycoon_core::skrifa::raw::FileRef;
7use maycoon_core::skrifa::setting::VariationSetting;
8use maycoon_core::skrifa::MetadataProvider;
9use maycoon_core::state::{State, Val};
10use maycoon_core::vg::peniko::{Brush, Fill};
11use maycoon_core::vg::{peniko, Glyph, Scene};
12use maycoon_core::widget::Widget;
13use maycoon_theme::id::WidgetId;
14use maycoon_theme::theme::Theme;
15use nalgebra::Vector2;
16
17/// Displays the given text with optional font, size and hinting.
18///
19/// See the [hello-world](https://github.com/maycoon-ui/maycoon/blob/master/examples/hello-world/src/main.rs) example for how to use it in practice.
20///
21/// ### Theming
22/// You can style the text with the following properties:
23/// - `color` - The color of the text.
24/// - `color_invert` - The color to use when the `invert_color` property is set to `true` in the theme [Globals].
25///
26/// [Globals]: maycoon_theme::globals::Globals
27pub struct Text<S: State> {
28    style: Val<S, LayoutStyle>,
29    text: Val<S, String>,
30    font: Val<S, Option<String>>,
31    font_size: Val<S, f32>,
32    hinting: Val<S, bool>,
33    line_gap: Val<S, f32>,
34}
35
36impl<S: State> Text<S> {
37    /// Create a new text widget with the given text.
38    pub fn new(text: impl Into<Val<S, String>>) -> Self {
39        Self {
40            style: LayoutStyle::default().into(),
41            text: text.into(),
42            font: None.into(),
43            font_size: 30.0.into(),
44            hinting: true.into(),
45            line_gap: 7.5.into(),
46        }
47    }
48
49    /// Set the hinting of the text.
50    ///
51    /// Hinting adjusts the display of an outline font so that it lines up with a rasterized grid.
52    /// At low screen resolutions and font size, hinting can produce clearer text.
53    pub fn with_hinting(mut self, hinting: impl Into<Val<S, bool>>) -> Self {
54        self.hinting = hinting.into();
55        self
56    }
57
58    /// Set the font of the text.
59    pub fn with_font(mut self, font: impl Into<Val<S, String>>) -> Self {
60        self.font = font.into().map(Some);
61        self
62    }
63
64    /// Set the font size of the text.
65    pub fn with_font_size(mut self, size: impl Into<Val<S, f32>>) -> Self {
66        self.font_size = size.into();
67        self
68    }
69
70    /// Set the line gap of the text.
71    ///
72    /// The line gap is the space between lines of text. Defaults to `7.5`.
73    pub fn with_line_gap(mut self, gap: impl Into<Val<S, f32>>) -> Self {
74        self.line_gap = gap.into();
75        self
76    }
77}
78
79impl<S: State> WidgetLayoutExt<S> for Text<S> {
80    fn set_layout_style(&mut self, layout_style: impl Into<Val<S, LayoutStyle>>) {
81        self.style = layout_style.into();
82    }
83}
84
85impl<S: State> Widget<S> for Text<S> {
86    fn render(
87        &mut self,
88        scene: &mut Scene,
89        theme: &mut dyn Theme,
90        info: &AppInfo,
91        layout_node: &LayoutNode,
92        state: &S,
93    ) {
94        let font_size = *self.font_size.get_ref(state);
95        let hinting = *self.hinting.get_ref(state);
96        let font_name = self.font.get_ref(state);
97
98        let font = if font_name.is_some() {
99            info.font_context
100                .get(font_name.clone().unwrap())
101                .expect("Font not found")
102        } else {
103            info.font_context.default_font().clone()
104        };
105
106        let font_ref = {
107            let file_ref = FileRef::new(font.data.as_ref()).expect("Failed to load font data");
108            match file_ref {
109                FileRef::Font(font) => Some(font),
110                FileRef::Collection(collection) => collection.get(font.index).ok(),
111            }
112        }
113        .expect("Failed to load font reference");
114
115        let color = if let Some(style) = theme.of(<Text<S> as Widget<S>>::widget_id(self)) {
116            if theme.globals().invert_text_color {
117                style.get_color("color_invert").unwrap()
118            } else {
119                style.get_color("color").unwrap()
120            }
121        } else {
122            theme.defaults().text().foreground()
123        };
124
125        let location = font_ref.axes().location::<&[VariationSetting; 0]>(&[]);
126
127        let metrics = font_ref.metrics(Size::new(font_size), &location);
128
129        let glyph_metrics = font_ref.glyph_metrics(Size::new(font_size), &location);
130
131        let line_height = metrics.ascent + metrics.descent + metrics.leading;
132
133        let line_gap = *self.line_gap.get_ref(state);
134
135        let charmap = font_ref.charmap();
136
137        let mut pen_x = layout_node.layout.location.x;
138
139        let mut pen_y = layout_node.layout.location.y + font_size;
140
141        let text = self.text.get_ref(state);
142
143        scene
144            .draw_glyphs(&font)
145            .font_size(font_size)
146            .brush(&Brush::Solid(color))
147            .normalized_coords(bytemuck::cast_slice(location.coords()))
148            .hint(hinting)
149            .draw(
150                &peniko::Style::Fill(Fill::NonZero),
151                text.chars().filter_map(|c| {
152                    if c == '\n' {
153                        pen_y += line_height + line_gap;
154                        pen_x = layout_node.layout.location.x;
155                        return None;
156                    }
157
158                    let gid = charmap.map(c).unwrap_or_default();
159                    let advance = glyph_metrics.advance_width(gid).unwrap_or_default();
160                    let x = pen_x;
161
162                    pen_x += advance;
163
164                    Some(Glyph {
165                        id: gid.to_u32(),
166                        x,
167                        y: pen_y,
168                    })
169                }),
170            );
171    }
172
173    fn layout_style(&mut self, state: &S) -> StyleNode {
174        let text = self.text.get_ref(state);
175
176        let font_size = *self.font_size.get_ref(state);
177
178        let style = self.style.get_ref(state).clone();
179
180        StyleNode {
181            style: LayoutStyle {
182                size: Vector2::new(
183                    Dimension::Length(font_size * text.len() as f32),
184                    Dimension::Length(font_size),
185                ),
186                ..style
187            },
188            children: Vec::new(),
189        }
190    }
191
192    fn update(&mut self, _: &LayoutNode, _: &mut S, _: &AppInfo) -> Update {
193        self.text.invalidate();
194        self.font.invalidate();
195        self.hinting.invalidate();
196        self.font_size.invalidate();
197        self.style.invalidate();
198        Update::empty()
199    }
200
201    fn widget_id(&self) -> WidgetId {
202        WidgetId::new("maycoon-widgets", "Text")
203    }
204}