Skip to main content

freya_core/elements/
label.rs

1//! Draw text with [label()]. Its a simplified version of [crate::elements::paragraph].
2
3use std::{
4    any::Any,
5    borrow::Cow,
6    rc::Rc,
7};
8
9use freya_engine::prelude::{
10    ClipOp,
11    FontStyle,
12    ParagraphBuilder,
13    ParagraphStyle,
14    SkParagraph,
15    SkRect,
16    TextStyle,
17};
18use rustc_hash::FxHashMap;
19use torin::prelude::Size2D;
20
21use crate::{
22    data::{
23        AccessibilityData,
24        EffectData,
25        LayoutData,
26        StyleState,
27        TextStyleData,
28    },
29    diff_key::DiffKey,
30    element::{
31        ClipContext,
32        Element,
33        ElementExt,
34        EventHandlerType,
35        LayoutContext,
36        RenderContext,
37    },
38    elements::paragraph::paint_paragraph_with_fill,
39    events::name::EventName,
40    layers::Layer,
41    prelude::{
42        AccessibilityExt,
43        Color,
44        ContainerExt,
45        EventHandlersExt,
46        KeyExt,
47        LayerExt,
48        LayoutExt,
49        MaybeExt,
50        Span,
51        TextAlign,
52        TextStyleExt,
53    },
54    text_cache::CachedParagraph,
55    tree::DiffModifies,
56};
57
58/// Draw text with [label()]. Its a simplified version of [crate::elements::paragraph].
59///
60/// See the available methods in [Label].
61///
62/// ```rust
63/// # use freya::prelude::*;
64/// fn app() -> impl IntoElement {
65///     label().text("Hello, world!").font_size(16.0)
66/// }
67/// ```
68pub fn label() -> Label {
69    Label {
70        key: DiffKey::None,
71        element: LabelElement::default(),
72    }
73}
74
75impl From<&str> for Element {
76    fn from(value: &str) -> Self {
77        label().text(value.to_string()).into()
78    }
79}
80
81impl From<String> for Element {
82    fn from(value: String) -> Self {
83        label().text(value).into()
84    }
85}
86
87/// Whether text sizes itself to its content or expands to the available width.
88pub enum TextWidth {
89    /// Shrink to fit the text content.
90    Fit,
91    /// Expand to the maximum available width.
92    Max,
93}
94
95#[derive(PartialEq, Clone)]
96pub struct LabelElement {
97    pub text: Cow<'static, str>,
98    pub accessibility: AccessibilityData,
99    pub text_style_data: TextStyleData,
100    pub layout: LayoutData,
101    pub event_handlers: FxHashMap<EventName, EventHandlerType>,
102    pub max_lines: Option<usize>,
103    pub line_height: Option<f32>,
104    pub relative_layer: Layer,
105}
106
107impl Default for LabelElement {
108    fn default() -> Self {
109        let mut accessibility = AccessibilityData::default();
110        accessibility.builder.set_role(accesskit::Role::Label);
111        Self {
112            text: Default::default(),
113            accessibility,
114            text_style_data: Default::default(),
115            layout: Default::default(),
116            event_handlers: Default::default(),
117            max_lines: None,
118            line_height: None,
119            relative_layer: Layer::default(),
120        }
121    }
122}
123
124impl ElementExt for LabelElement {
125    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
126        let Some(label) = (other.as_ref() as &dyn Any).downcast_ref::<LabelElement>() else {
127            return false;
128        };
129        self != label
130    }
131
132    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
133        let Some(label) = (other.as_ref() as &dyn Any).downcast_ref::<LabelElement>() else {
134            return DiffModifies::all();
135        };
136
137        let mut diff = DiffModifies::empty();
138
139        if self.text != label.text {
140            diff.insert(DiffModifies::STYLE);
141            diff.insert(DiffModifies::LAYOUT);
142        }
143
144        if self.accessibility != label.accessibility {
145            diff.insert(DiffModifies::ACCESSIBILITY);
146        }
147
148        if self.relative_layer != label.relative_layer {
149            diff.insert(DiffModifies::LAYER);
150        }
151
152        if self.text_style_data != label.text_style_data
153            || self.line_height != label.line_height
154            || self.max_lines != label.max_lines
155        {
156            diff.insert(DiffModifies::TEXT_STYLE);
157            diff.insert(DiffModifies::LAYOUT);
158        }
159        if self.layout != label.layout {
160            diff.insert(DiffModifies::LAYOUT);
161        }
162
163        if self.event_handlers != label.event_handlers {
164            diff.insert(DiffModifies::EVENT_HANDLERS);
165        }
166
167        diff
168    }
169
170    fn layout(&'_ self) -> Cow<'_, LayoutData> {
171        Cow::Borrowed(&self.layout)
172    }
173
174    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
175        None
176    }
177
178    fn style(&'_ self) -> Cow<'_, StyleState> {
179        Cow::Owned(StyleState::default())
180    }
181
182    fn is_transparent(&self) -> bool {
183        false
184    }
185
186    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
187        Cow::Borrowed(&self.text_style_data)
188    }
189
190    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
191        Cow::Borrowed(&self.accessibility)
192    }
193
194    fn layer(&self) -> Layer {
195        self.relative_layer
196    }
197
198    fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
199        Some(Cow::Borrowed(&self.event_handlers))
200    }
201
202    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
203        let cached_paragraph = CachedParagraph {
204            text_style_state: context.text_style_state,
205            spans: &[Span::new(&*self.text)],
206            max_lines: None,
207            line_height: None,
208            width: context.area_size.width,
209        };
210        let paragraph = context
211            .text_cache
212            .utilize(context.node_id, &cached_paragraph)
213            .unwrap_or_else(|| {
214                let mut paragraph_style = ParagraphStyle::default();
215                let mut text_style = TextStyle::default();
216
217                let mut font_families = context.text_style_state.font_families.clone();
218                font_families.extend_from_slice(context.fallback_fonts);
219
220                text_style.set_color(
221                    context
222                        .text_style_state
223                        .color
224                        .as_color()
225                        .unwrap_or(Color::WHITE),
226                );
227                text_style.set_font_size(
228                    f32::from(context.text_style_state.font_size) * context.scale_factor as f32,
229                );
230                text_style.set_font_families(&font_families);
231                text_style.set_font_style(FontStyle::new(
232                    context.text_style_state.font_weight.into(),
233                    context.text_style_state.font_width.into(),
234                    context.text_style_state.font_slant.into(),
235                ));
236
237                if context.text_style_state.text_height.needs_custom_height() {
238                    text_style.set_height_override(true);
239                    text_style.set_half_leading(true);
240                }
241
242                if let Some(line_height) = self.line_height {
243                    text_style.set_height_override(true).set_height(line_height);
244                }
245
246                for text_shadow in context.text_style_state.text_shadows.iter() {
247                    text_style.add_shadow((*text_shadow).into());
248                }
249
250                if let Some(ellipsis) = context.text_style_state.text_overflow.get_ellipsis() {
251                    paragraph_style.set_ellipsis(ellipsis);
252                }
253
254                paragraph_style.set_text_style(&text_style);
255                paragraph_style.set_max_lines(self.max_lines);
256                paragraph_style.set_text_align(context.text_style_state.text_align.into());
257
258                let mut paragraph_builder =
259                    ParagraphBuilder::new(&paragraph_style, &*context.font_collection);
260
261                paragraph_builder.add_text(&self.text);
262
263                let mut paragraph = paragraph_builder.build();
264                paragraph.layout(
265                    if self.max_lines == Some(1)
266                        && context.text_style_state.text_align == TextAlign::default()
267                        && !paragraph_style.ellipsized()
268                    {
269                        f32::MAX
270                    } else {
271                        context.area_size.width + 1.0
272                    },
273                );
274
275                context
276                    .text_cache
277                    .insert(context.node_id, &cached_paragraph, paragraph)
278            });
279
280        let size = Size2D::new(paragraph.longest_line(), paragraph.height()).max(Size2D::zero());
281
282        Some((size, paragraph))
283    }
284
285    fn should_hook_measurement(&self) -> bool {
286        true
287    }
288
289    fn should_measure_inner_children(&self) -> bool {
290        false
291    }
292
293    fn clip(&self, context: ClipContext) {
294        let area = context.visible_area;
295        context.canvas.clip_rect(
296            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
297            ClipOp::Intersect,
298            true,
299        );
300    }
301
302    fn render(&self, context: RenderContext) {
303        let layout_data = context.layout_node.data.as_ref().unwrap();
304        let paragraph = layout_data.downcast_ref::<SkParagraph>().unwrap();
305
306        paint_paragraph_with_fill(
307            paragraph,
308            context.canvas,
309            context.layout_node.visible_area().origin,
310            &context.text_style_state.color,
311        );
312    }
313}
314
315impl From<Label> for Element {
316    fn from(value: Label) -> Self {
317        Element::Element {
318            key: value.key,
319            element: Rc::new(value.element),
320            elements: vec![],
321        }
322    }
323}
324
325impl KeyExt for Label {
326    fn write_key(&mut self) -> &mut DiffKey {
327        &mut self.key
328    }
329}
330
331impl EventHandlersExt for Label {
332    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
333        &mut self.element.event_handlers
334    }
335}
336
337impl AccessibilityExt for Label {
338    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
339        &mut self.element.accessibility
340    }
341}
342
343impl TextStyleExt for Label {
344    fn get_text_style_data(&mut self) -> &mut TextStyleData {
345        &mut self.element.text_style_data
346    }
347}
348
349impl LayerExt for Label {
350    fn get_layer(&mut self) -> &mut Layer {
351        &mut self.element.relative_layer
352    }
353}
354
355impl MaybeExt for Label {}
356
357pub struct Label {
358    key: DiffKey,
359    element: LabelElement,
360}
361
362impl Label {
363    pub fn try_downcast(element: &dyn ElementExt) -> Option<LabelElement> {
364        (element as &dyn Any)
365            .downcast_ref::<LabelElement>()
366            .cloned()
367    }
368
369    /// Set the text content of the label.
370    pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self {
371        let text = text.into();
372        self.element.text = text;
373        self
374    }
375
376    /// Limit the label to at most this many lines, truncating the rest. Pass `None` for no limit.
377    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
378        self.element.max_lines = max_lines.into();
379        self
380    }
381
382    /// Override the height of each line as a multiple of the font size. Pass `None` for the default.
383    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
384        self.element.line_height = line_height.into();
385        self
386    }
387}
388
389impl LayoutExt for Label {
390    fn get_layout(&mut self) -> &mut LayoutData {
391        &mut self.element.layout
392    }
393}
394
395impl ContainerExt for Label {}