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    SizeModel,
20};
21
22use crate::{
23    data::{
24        AccessibilityData,
25        EffectData,
26        LayoutData,
27        StyleState,
28        TextStyleData,
29    },
30    diff_key::DiffKey,
31    element::{
32        ClipContext,
33        Element,
34        ElementExt,
35        EventHandlers,
36        LayoutContext,
37        RenderContext,
38    },
39    elements::paragraph::ParagraphPaintExt,
40    layers::Layer,
41    prelude::{
42        AccessibilityExt,
43        ContainerExt,
44        EffectExt,
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::default()
70}
71
72impl From<&str> for Element {
73    fn from(value: &str) -> Self {
74        label().text(value.to_string()).into()
75    }
76}
77
78impl From<String> for Element {
79    fn from(value: String) -> Self {
80        label().text(value).into()
81    }
82}
83
84/// Whether text sizes itself to its content or expands to the available width.
85pub enum TextWidth {
86    /// Shrink to fit the text content.
87    Fit,
88    /// Expand to the maximum available width.
89    Max,
90}
91
92#[derive(PartialEq, Clone)]
93pub struct LabelElement {
94    pub text: Cow<'static, str>,
95    pub accessibility: AccessibilityData,
96    pub text_style_data: TextStyleData,
97    pub layout: LayoutData,
98    pub event_handlers: EventHandlers,
99    pub max_lines: Option<usize>,
100    pub line_height: Option<f32>,
101    pub relative_layer: Layer,
102    pub effect: Option<EffectData>,
103}
104
105impl Default for LabelElement {
106    fn default() -> Self {
107        let mut accessibility = AccessibilityData::default();
108        accessibility.builder.set_role(accesskit::Role::Label);
109        Self {
110            text: Default::default(),
111            accessibility,
112            text_style_data: Default::default(),
113            layout: Default::default(),
114            event_handlers: Default::default(),
115            max_lines: None,
116            line_height: None,
117            relative_layer: Layer::default(),
118            effect: None,
119        }
120    }
121}
122
123impl ElementExt for LabelElement {
124    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
125        let Some(label) = (other.as_ref() as &dyn Any).downcast_ref::<LabelElement>() else {
126            return false;
127        };
128        self != label
129    }
130
131    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
132        let Some(label) = (other.as_ref() as &dyn Any).downcast_ref::<LabelElement>() else {
133            return DiffModifies::all();
134        };
135
136        let mut diff = DiffModifies::empty();
137
138        if self.text != label.text {
139            diff.insert(DiffModifies::STYLE);
140            diff.insert(DiffModifies::LAYOUT);
141            diff.insert(DiffModifies::ACCESSIBILITY);
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.effect != label.effect {
153            diff.insert(DiffModifies::EFFECT);
154        }
155
156        if self.text_style_data != label.text_style_data
157            || self.line_height != label.line_height
158            || self.max_lines != label.max_lines
159        {
160            diff.insert(DiffModifies::TEXT_STYLE);
161            diff.insert(DiffModifies::LAYOUT);
162        }
163        if self.layout != label.layout {
164            diff.insert(DiffModifies::LAYOUT);
165        }
166
167        if self.event_handlers != label.event_handlers {
168            diff.insert(DiffModifies::EVENT_HANDLERS);
169        }
170
171        diff
172    }
173
174    fn layout(&'_ self) -> Cow<'_, LayoutData> {
175        Cow::Borrowed(&self.layout)
176    }
177
178    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
179        self.effect.as_ref().map(Cow::Borrowed)
180    }
181
182    fn style(&'_ self) -> Cow<'_, StyleState> {
183        Cow::Owned(StyleState::default())
184    }
185
186    fn is_transparent(&self) -> bool {
187        false
188    }
189
190    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
191        Cow::Borrowed(&self.text_style_data)
192    }
193
194    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
195        Cow::Borrowed(&self.accessibility)
196    }
197
198    fn finish_accessibility(&self, builder: &mut accesskit::Node) {
199        builder.set_value(self.text.clone());
200    }
201
202    fn layer(&self) -> Layer {
203        self.relative_layer
204    }
205
206    fn events_handlers(&'_ self) -> Option<Cow<'_, EventHandlers>> {
207        Some(Cow::Borrowed(&self.event_handlers))
208    }
209
210    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
211        let content_area_size =
212            (*context.area_size - context.torin_node.padding.into()).max(Size2D::zero());
213        let cached_paragraph = CachedParagraph {
214            text_style_state: context.text_style_state,
215            spans: &[Span::new(&*self.text)],
216            max_lines: self.max_lines,
217            line_height: self.line_height,
218            width: content_area_size.width,
219        };
220        let paragraph = context
221            .text_cache
222            .utilize(context.node_id, &cached_paragraph)
223            .unwrap_or_else(|| {
224                let build = |fill_area: Area| {
225                    let mut paragraph_style = ParagraphStyle::default();
226
227                    if let Some(ellipsis) = context.text_style_state.text_overflow.get_ellipsis() {
228                        paragraph_style.set_ellipsis(ellipsis);
229                    }
230
231                    let text_style = context.text_style_state.to_text_style(
232                        context.fallback_fonts,
233                        context.scale_factor,
234                        self.line_height,
235                        fill_area,
236                    );
237                    paragraph_style.set_text_style(&text_style);
238                    paragraph_style.set_max_lines(self.max_lines);
239                    paragraph_style.set_text_align(context.text_style_state.text_align.into());
240
241                    let mut paragraph_builder =
242                        ParagraphBuilder::new(&paragraph_style, &*context.font_collection);
243
244                    paragraph_builder.push_style(&text_style);
245                    paragraph_builder.add_text(&self.text);
246
247                    let mut paragraph = paragraph_builder.build();
248                    paragraph.layout(
249                        if self.max_lines == Some(1)
250                            && context.text_style_state.text_align == TextAlign::default()
251                            && !paragraph_style.ellipsized()
252                        {
253                            f32::MAX
254                        } else {
255                            content_area_size.width + 1.0
256                        },
257                    );
258                    paragraph
259                };
260
261                let mut paragraph = build(Area::default());
262
263                if context.text_style_state.color.as_color().is_none() {
264                    paragraph = build(paragraph.fill_area());
265                }
266
267                context
268                    .text_cache
269                    .insert(context.node_id, &cached_paragraph, paragraph)
270            });
271
272        let size = Size2D::new(paragraph.longest_line(), paragraph.height())
273            .max(Size2D::zero())
274            .with_gaps(&context.torin_node.padding);
275
276        Some((size, paragraph))
277    }
278
279    fn should_hook_measurement(&self) -> bool {
280        true
281    }
282
283    fn should_measure_inner_children(&self) -> bool {
284        false
285    }
286
287    fn clip(&self, context: ClipContext) {
288        let area = context.visible_area;
289        context.canvas.clip_rect(
290            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
291            ClipOp::Intersect,
292            true,
293        );
294    }
295
296    fn render(&self, context: RenderContext) {
297        let layout_data = context.layout_node.data.as_ref().unwrap();
298        let paragraph = layout_data.downcast_ref::<SkParagraph>().unwrap();
299
300        let origin = context.layout_node.inner_area.origin;
301        paragraph.paint_at(context.canvas, origin.cast_unit());
302    }
303}
304
305impl From<Label> for Element {
306    fn from(value: Label) -> Self {
307        Element::Element {
308            key: value.key,
309            element: Rc::new(value.element),
310            elements: vec![],
311        }
312    }
313}
314
315impl KeyExt for Label {
316    fn key_mut(&mut self) -> &mut DiffKey {
317        &mut self.key
318    }
319}
320
321impl EventHandlersExt for Label {
322    fn event_handlers_mut(&mut self) -> &mut EventHandlers {
323        &mut self.element.event_handlers
324    }
325}
326
327impl AccessibilityExt for Label {
328    fn accessibility_data_mut(&mut self) -> &mut AccessibilityData {
329        &mut self.element.accessibility
330    }
331}
332
333impl TextStyleExt for Label {
334    fn text_style_data_mut(&mut self) -> &mut TextStyleData {
335        &mut self.element.text_style_data
336    }
337}
338
339impl EffectExt for Label {
340    fn effect_mut(&mut self) -> &mut EffectData {
341        self.element.effect.get_or_insert_with(EffectData::default)
342    }
343}
344
345impl LayerExt for Label {
346    fn layer_mut(&mut self) -> &mut Layer {
347        &mut self.element.relative_layer
348    }
349}
350
351impl MaybeExt for Label {}
352
353#[derive(Default, Clone)]
354pub struct Label {
355    key: DiffKey,
356    element: LabelElement,
357}
358
359impl Label {
360    pub fn try_downcast(element: &dyn ElementExt) -> Option<LabelElement> {
361        (element as &dyn Any)
362            .downcast_ref::<LabelElement>()
363            .cloned()
364    }
365
366    /// Set the text content of the label.
367    pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self {
368        let text = text.into();
369        self.element.text = text;
370        self
371    }
372
373    /// Limit the label to at most this many lines, truncating the rest. Pass `None` for no limit.
374    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
375        self.element.max_lines = max_lines.into();
376        self
377    }
378
379    /// Override the height of each line as a multiple of the font size. Pass `None` for the default.
380    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
381        self.element.line_height = line_height.into();
382        self
383    }
384}
385
386impl LayoutExt for Label {
387    fn layout_mut(&mut self) -> &mut LayoutData {
388        &mut self.element.layout
389    }
390}
391
392impl ContainerExt for Label {}