maycoon_widgets/
text.rs

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