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;
5use maycoon_core::layout::{Dimension, LayoutNode, LayoutStyle, StyleNode};
6use maycoon_core::signal::MaybeSignal;
7use maycoon_core::vgi::{Brush, Scene};
8use maycoon_core::widget::{Widget, WidgetLayoutExt};
9use maycoon_theme::id::WidgetId;
10use maycoon_theme::theme::Theme;
11use nalgebra::Vector2;
12use std::ops::Deref;
13
14/// Displays the given text with optional font, size and hinting.
15///
16/// 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.
17///
18/// ### Theming
19/// You can style the text with the following properties:
20/// - `color` - The color of the text.
21/// - `color_invert` - The color to use when the `invert_color` property is set to `true` in the theme [Globals].
22///
23/// The [WidgetId] is equal to `maycoon-widgets:Text`.
24///
25/// [Globals]: maycoon_theme::globals::Globals
26pub struct Text {
27    style: MaybeSignal<LayoutStyle>,
28    text: MaybeSignal<String>,
29    font: MaybeSignal<Option<String>>,
30    font_size: MaybeSignal<f32>,
31    line_gap: MaybeSignal<f32>,
32    wrap: MaybeSignal<bool>,
33    hinting: MaybeSignal<bool>,
34    max_width: f32,
35}
36
37impl Text {
38    /// Create a new text widget with the given text.
39    #[inline(always)]
40    pub fn new(text: impl Into<MaybeSignal<String>>) -> Self {
41        Self {
42            style: LayoutStyle::default().into(),
43            text: text.into(),
44            font: None.into(),
45            font_size: 30.0.into(),
46            line_gap: 7.5.into(),
47            wrap: true.into(),
48            hinting: true.into(),
49            max_width: 0.0,
50        }
51    }
52
53    /// Set whether to wrap the text.
54    #[inline(always)]
55    pub fn with_wrap(mut self, linebreaks: impl Into<MaybeSignal<bool>>) -> Self {
56        self.wrap = linebreaks.into();
57        self
58    }
59
60    /// Set the hinting of the text.
61    ///
62    /// Hinting adjusts the display of an outline font so that it lines up with a rasterized grid.
63    /// At low screen resolutions and font size, hinting can produce clearer text.
64    #[inline(always)]
65    pub fn with_hinting(mut self, hinting: impl Into<MaybeSignal<bool>>) -> Self {
66        self.hinting = hinting.into();
67        self
68    }
69
70    /// Set the font of the text.
71    #[inline(always)]
72    pub fn with_font(mut self, font: impl Into<MaybeSignal<Option<String>>>) -> Self {
73        self.font = font.into();
74        self
75    }
76
77    /// Set the font size of the text.
78    #[inline(always)]
79    pub fn with_font_size(mut self, size: impl Into<MaybeSignal<f32>>) -> Self {
80        self.font_size = size.into();
81        self
82    }
83
84    /// Set the line gap of the text.
85    ///
86    /// The line gap is the space between lines of text. Defaults to `7.5`.
87    #[inline(always)]
88    pub fn with_line_gap(mut self, gap: impl Into<MaybeSignal<f32>>) -> Self {
89        self.line_gap = gap.into();
90        self
91    }
92}
93
94impl WidgetLayoutExt for Text {
95    #[inline(always)]
96    fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
97        self.style = layout_style.into();
98    }
99}
100
101impl Widget for Text {
102    fn render(
103        &mut self,
104        scene: &mut dyn Scene,
105        theme: &mut dyn Theme,
106        layout_node: &LayoutNode,
107        info: &AppInfo,
108        _: AppContext,
109    ) {
110        let font_name = self.font.get();
111
112        let font = if font_name.is_some() {
113            info.font_context
114                .get(font_name.deref().clone().unwrap())
115                .expect("Font not found")
116        } else {
117            info.font_context.default_font().clone()
118        };
119
120        let color = if let Some(style) = theme.of(Self::widget_id(self)) {
121            if theme.globals().invert_text_color {
122                style.get_color("color_invert").unwrap()
123            } else {
124                style.get_color("color").unwrap()
125            }
126        } else {
127            theme.defaults().text().foreground()
128        };
129
130        if *self.wrap.get() {
131            self.max_width = layout_node.layout.size.width;
132        } else {
133            self.max_width = f32::INFINITY;
134        }
135
136        scene.draw_text(
137            &Brush::Solid(color),
138            None,
139            Vector2::new(layout_node.layout.location.x, layout_node.layout.location.y),
140            self.text.get().as_str(),
141            *self.hinting.get(),
142            &font,
143            *self.font_size.get(),
144            *self.line_gap.get(),
145            self.max_width,
146        );
147    }
148
149    fn layout_style(&self) -> StyleNode {
150        let text = self.text.get();
151
152        let font_size = *self.font_size.get();
153
154        let style = self.style.get().deref().clone();
155
156        StyleNode {
157            style: LayoutStyle {
158                size: Vector2::new(
159                    Dimension::length(font_size * text.len() as f32),
160                    Dimension::length(font_size),
161                ),
162                ..style
163            },
164            children: Vec::new(),
165        }
166    }
167
168    #[inline(always)]
169    fn update(&mut self, layout: &LayoutNode, _: AppContext, _: &AppInfo) -> Update {
170        // Re-layout if the maximum width of the text changes.
171        if *self.wrap.get() && !layout::equal(layout.layout.size.width, self.max_width) {
172            Update::LAYOUT
173        } else {
174            Update::empty()
175        }
176    }
177
178    #[inline(always)]
179    fn widget_id(&self) -> WidgetId {
180        WidgetId::new("maycoon-widgets", "Text")
181    }
182}