freya_core/elements/
label.rs

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