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