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 torin::prelude::Size2D;
19
20use crate::{
21    data::{
22        AccessibilityData,
23        EffectData,
24        LayoutData,
25        StyleState,
26        TextStyleData,
27    },
28    diff_key::DiffKey,
29    element::{
30        ClipContext,
31        Element,
32        ElementExt,
33        EventHandlers,
34        LayoutContext,
35        RenderContext,
36    },
37    elements::paragraph::paint_paragraph_with_fill,
38    layers::Layer,
39    prelude::{
40        AccessibilityExt,
41        Color,
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        }
138
139        if self.accessibility != label.accessibility {
140            diff.insert(DiffModifies::ACCESSIBILITY);
141        }
142
143        if self.relative_layer != label.relative_layer {
144            diff.insert(DiffModifies::LAYER);
145        }
146
147        if self.text_style_data != label.text_style_data
148            || self.line_height != label.line_height
149            || self.max_lines != label.max_lines
150        {
151            diff.insert(DiffModifies::TEXT_STYLE);
152            diff.insert(DiffModifies::LAYOUT);
153        }
154        if self.layout != label.layout {
155            diff.insert(DiffModifies::LAYOUT);
156        }
157
158        if self.event_handlers != label.event_handlers {
159            diff.insert(DiffModifies::EVENT_HANDLERS);
160        }
161
162        diff
163    }
164
165    fn layout(&'_ self) -> Cow<'_, LayoutData> {
166        Cow::Borrowed(&self.layout)
167    }
168
169    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
170        None
171    }
172
173    fn style(&'_ self) -> Cow<'_, StyleState> {
174        Cow::Owned(StyleState::default())
175    }
176
177    fn is_transparent(&self) -> bool {
178        false
179    }
180
181    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
182        Cow::Borrowed(&self.text_style_data)
183    }
184
185    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
186        Cow::Borrowed(&self.accessibility)
187    }
188
189    fn layer(&self) -> Layer {
190        self.relative_layer
191    }
192
193    fn events_handlers(&'_ self) -> Option<Cow<'_, EventHandlers>> {
194        Some(Cow::Borrowed(&self.event_handlers))
195    }
196
197    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
198        let cached_paragraph = CachedParagraph {
199            text_style_state: context.text_style_state,
200            spans: &[Span::new(&*self.text)],
201            max_lines: None,
202            line_height: None,
203            width: context.area_size.width,
204        };
205        let paragraph = context
206            .text_cache
207            .utilize(context.node_id, &cached_paragraph)
208            .unwrap_or_else(|| {
209                let mut paragraph_style = ParagraphStyle::default();
210                let mut text_style = TextStyle::default();
211
212                let mut font_families = context.text_style_state.font_families.clone();
213                font_families.extend_from_slice(context.fallback_fonts);
214
215                text_style.set_color(
216                    context
217                        .text_style_state
218                        .color
219                        .as_color()
220                        .unwrap_or(Color::WHITE),
221                );
222                text_style.set_font_size(
223                    f32::from(context.text_style_state.font_size) * context.scale_factor as f32,
224                );
225                text_style.set_font_families(&font_families);
226                text_style.set_font_style(FontStyle::new(
227                    context.text_style_state.font_weight.into(),
228                    context.text_style_state.font_width.into(),
229                    context.text_style_state.font_slant.into(),
230                ));
231
232                if context.text_style_state.text_height.needs_custom_height() {
233                    text_style.set_height_override(true);
234                    text_style.set_half_leading(true);
235                }
236
237                if let Some(line_height) = self.line_height {
238                    text_style.set_height_override(true).set_height(line_height);
239                }
240
241                for text_shadow in context.text_style_state.text_shadows.iter() {
242                    text_style.add_shadow((*text_shadow).into());
243                }
244
245                if let Some(ellipsis) = context.text_style_state.text_overflow.get_ellipsis() {
246                    paragraph_style.set_ellipsis(ellipsis);
247                }
248
249                paragraph_style.set_text_style(&text_style);
250                paragraph_style.set_max_lines(self.max_lines);
251                paragraph_style.set_text_align(context.text_style_state.text_align.into());
252
253                let mut paragraph_builder =
254                    ParagraphBuilder::new(&paragraph_style, &*context.font_collection);
255
256                paragraph_builder.add_text(&self.text);
257
258                let mut paragraph = paragraph_builder.build();
259                paragraph.layout(
260                    if self.max_lines == Some(1)
261                        && context.text_style_state.text_align == TextAlign::default()
262                        && !paragraph_style.ellipsized()
263                    {
264                        f32::MAX
265                    } else {
266                        context.area_size.width + 1.0
267                    },
268                );
269
270                context
271                    .text_cache
272                    .insert(context.node_id, &cached_paragraph, paragraph)
273            });
274
275        let size = Size2D::new(paragraph.longest_line(), paragraph.height()).max(Size2D::zero());
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        paint_paragraph_with_fill(
302            paragraph,
303            context.canvas,
304            context.layout_node.visible_area().origin,
305            &context.text_style_state.color,
306        );
307    }
308}
309
310impl From<Label> for Element {
311    fn from(value: Label) -> Self {
312        Element::Element {
313            key: value.key,
314            element: Rc::new(value.element),
315            elements: vec![],
316        }
317    }
318}
319
320impl KeyExt for Label {
321    fn write_key(&mut self) -> &mut DiffKey {
322        &mut self.key
323    }
324}
325
326impl EventHandlersExt for Label {
327    fn get_event_handlers(&mut self) -> &mut EventHandlers {
328        &mut self.element.event_handlers
329    }
330}
331
332impl AccessibilityExt for Label {
333    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
334        &mut self.element.accessibility
335    }
336}
337
338impl TextStyleExt for Label {
339    fn get_text_style_data(&mut self) -> &mut TextStyleData {
340        &mut self.element.text_style_data
341    }
342}
343
344impl LayerExt for Label {
345    fn get_layer(&mut self) -> &mut Layer {
346        &mut self.element.relative_layer
347    }
348}
349
350impl MaybeExt for Label {}
351
352#[derive(Default)]
353pub struct Label {
354    key: DiffKey,
355    element: LabelElement,
356}
357
358impl Label {
359    pub fn try_downcast(element: &dyn ElementExt) -> Option<LabelElement> {
360        (element as &dyn Any)
361            .downcast_ref::<LabelElement>()
362            .cloned()
363    }
364
365    /// Set the text content of the label.
366    pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self {
367        let text = text.into();
368        self.element.text = text;
369        self
370    }
371
372    /// Limit the label to at most this many lines, truncating the rest. Pass `None` for no limit.
373    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
374        self.element.max_lines = max_lines.into();
375        self
376    }
377
378    /// Override the height of each line as a multiple of the font size. Pass `None` for the default.
379    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
380        self.element.line_height = line_height.into();
381        self
382    }
383}
384
385impl LayoutExt for Label {
386    fn get_layout(&mut self) -> &mut LayoutData {
387        &mut self.element.layout
388    }
389}
390
391impl ContainerExt for Label {}