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        TextOverflowExt,
53        TextStyleExt,
54    },
55    text_cache::CachedParagraph,
56    tree::DiffModifies,
57};
58
59/// Draw text with [label()]. Its a simplified version of [crate::elements::paragraph].
60///
61/// See the available methods in [Label].
62///
63/// ```rust
64/// # use freya::prelude::*;
65/// fn app() -> impl IntoElement {
66///     label().text("Hello, world!").font_size(16.0)
67/// }
68/// ```
69pub fn label() -> Label {
70    Label::default()
71}
72
73impl From<&str> for Element {
74    fn from(value: &str) -> Self {
75        label().text(value.to_string()).into()
76    }
77}
78
79impl From<String> for Element {
80    fn from(value: String) -> Self {
81        label().text(value).into()
82    }
83}
84
85/// Whether text sizes itself to its content or expands to the available width.
86pub enum TextWidth {
87    /// Shrink to fit the text content.
88    Fit,
89    /// Expand to the maximum available width.
90    Max,
91}
92
93#[derive(PartialEq, Clone)]
94pub struct LabelElement {
95    pub text: Cow<'static, str>,
96    pub accessibility: AccessibilityData,
97    pub text_style_data: TextStyleData,
98    pub layout: LayoutData,
99    pub event_handlers: EventHandlers,
100    pub max_lines: Option<usize>,
101    pub line_height: Option<f32>,
102    pub relative_layer: Layer,
103    pub effect: Option<EffectData>,
104}
105
106impl Default for LabelElement {
107    fn default() -> Self {
108        let mut accessibility = AccessibilityData::default();
109        accessibility.builder.set_role(accesskit::Role::Label);
110        Self {
111            text: Default::default(),
112            accessibility,
113            text_style_data: Default::default(),
114            layout: Default::default(),
115            event_handlers: Default::default(),
116            max_lines: None,
117            line_height: None,
118            relative_layer: Layer::default(),
119            effect: None,
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            diff.insert(DiffModifies::ACCESSIBILITY);
143        }
144
145        if self.accessibility != label.accessibility {
146            diff.insert(DiffModifies::ACCESSIBILITY);
147        }
148
149        if self.relative_layer != label.relative_layer {
150            diff.insert(DiffModifies::LAYER);
151        }
152
153        if self.effect != label.effect {
154            diff.insert(DiffModifies::EFFECT);
155        }
156
157        if self.text_style_data != label.text_style_data
158            || self.line_height != label.line_height
159            || self.max_lines != label.max_lines
160        {
161            diff.insert(DiffModifies::TEXT_STYLE);
162            diff.insert(DiffModifies::LAYOUT);
163        }
164        if self.layout != label.layout {
165            diff.insert(DiffModifies::LAYOUT);
166        }
167
168        if self.event_handlers != label.event_handlers {
169            diff.insert(DiffModifies::EVENT_HANDLERS);
170        }
171
172        diff
173    }
174
175    fn layout(&'_ self) -> Cow<'_, LayoutData> {
176        Cow::Borrowed(&self.layout)
177    }
178
179    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
180        self.effect.as_ref().map(Cow::Borrowed)
181    }
182
183    fn style(&'_ self) -> Cow<'_, StyleState> {
184        Cow::Owned(StyleState::default())
185    }
186
187    fn is_transparent(&self) -> bool {
188        false
189    }
190
191    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
192        Cow::Borrowed(&self.text_style_data)
193    }
194
195    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
196        Cow::Borrowed(&self.accessibility)
197    }
198
199    fn finish_accessibility(&self, builder: &mut accesskit::Node) {
200        builder.set_value(self.text.clone());
201    }
202
203    fn layer(&self) -> Layer {
204        self.relative_layer
205    }
206
207    fn events_handlers(&'_ self) -> Option<Cow<'_, EventHandlers>> {
208        Some(Cow::Borrowed(&self.event_handlers))
209    }
210
211    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
212        let content_area_size =
213            (*context.area_size - context.torin_node.padding.into()).max(Size2D::zero());
214        let cached_paragraph = CachedParagraph {
215            text_style_state: context.text_style_state,
216            spans: &[Span::new(&*self.text)],
217            max_lines: self.max_lines,
218            line_height: self.line_height,
219            width: content_area_size.width,
220        };
221        let paragraph = context
222            .text_cache
223            .utilize(context.node_id, &cached_paragraph)
224            .unwrap_or_else(|| {
225                let build = |fill_area: Area| {
226                    let mut paragraph_style = ParagraphStyle::default();
227
228                    if let Some(ellipsis) = context.text_style_state.text_overflow.get_ellipsis() {
229                        paragraph_style.set_ellipsis(ellipsis);
230                    }
231
232                    let text_style = context.text_style_state.to_text_style(
233                        context.fallback_fonts,
234                        context.scale_factor,
235                        self.line_height,
236                        fill_area,
237                    );
238                    paragraph_style.set_text_style(&text_style);
239                    paragraph_style.set_max_lines(self.max_lines);
240                    paragraph_style.set_text_align(context.text_style_state.text_align.into());
241
242                    let mut paragraph_builder =
243                        ParagraphBuilder::new(&paragraph_style, &*context.font_collection);
244
245                    paragraph_builder.push_style(&text_style);
246                    paragraph_builder.add_text(&self.text);
247
248                    let mut paragraph = paragraph_builder.build();
249                    paragraph.layout(
250                        if self.max_lines == Some(1)
251                            && context.text_style_state.text_align == TextAlign::default()
252                            && !paragraph_style.ellipsized()
253                        {
254                            f32::MAX
255                        } else {
256                            content_area_size.width + 1.0
257                        },
258                    );
259                    paragraph
260                };
261
262                let mut paragraph = build(Area::default());
263
264                if context.text_style_state.color.as_color().is_none() {
265                    paragraph = build(paragraph.fill_area());
266                }
267
268                context
269                    .text_cache
270                    .insert(context.node_id, &cached_paragraph, paragraph)
271            });
272
273        let size = Size2D::new(paragraph.longest_line(), paragraph.height())
274            .max(Size2D::zero())
275            .with_gaps(&context.torin_node.padding);
276
277        Some((size, paragraph))
278    }
279
280    fn should_hook_measurement(&self) -> bool {
281        true
282    }
283
284    fn should_measure_inner_children(&self) -> bool {
285        false
286    }
287
288    fn clip(&self, context: ClipContext) {
289        let area = context.visible_area;
290        context.canvas.clip_rect(
291            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
292            ClipOp::Intersect,
293            true,
294        );
295    }
296
297    fn render(&self, context: RenderContext) {
298        let layout_data = context.layout_node.data.as_ref().unwrap();
299        let paragraph = layout_data.downcast_ref::<SkParagraph>().unwrap();
300
301        let origin = context.layout_node.inner_area.origin;
302        paragraph.paint_at(context.canvas, origin.cast_unit());
303    }
304}
305
306impl From<Label> for Element {
307    fn from(value: Label) -> Self {
308        Element::Element {
309            key: value.key,
310            element: Rc::new(value.element),
311            elements: vec![],
312        }
313    }
314}
315
316impl KeyExt for Label {
317    fn key_mut(&mut self) -> &mut DiffKey {
318        &mut self.key
319    }
320}
321
322impl EventHandlersExt for Label {
323    fn event_handlers_mut(&mut self) -> &mut EventHandlers {
324        &mut self.element.event_handlers
325    }
326}
327
328impl AccessibilityExt for Label {
329    fn accessibility_data_mut(&mut self) -> &mut AccessibilityData {
330        &mut self.element.accessibility
331    }
332}
333
334impl TextOverflowExt for Label {}
335
336impl TextStyleExt for Label {
337    fn text_style_data_mut(&mut self) -> &mut TextStyleData {
338        &mut self.element.text_style_data
339    }
340}
341
342impl EffectExt for Label {
343    fn effect_mut(&mut self) -> &mut EffectData {
344        self.element.effect.get_or_insert_with(EffectData::default)
345    }
346}
347
348impl LayerExt for Label {
349    fn layer_mut(&mut self) -> &mut Layer {
350        &mut self.element.relative_layer
351    }
352}
353
354impl MaybeExt for Label {}
355
356#[derive(Default, Clone)]
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 layout_mut(&mut self) -> &mut LayoutData {
391        &mut self.element.layout
392    }
393}
394
395impl ContainerExt for Label {}