Skip to main content

freya_core/
element.rs

1use std::{
2    any::{
3        Any,
4        TypeId,
5    },
6    borrow::Cow,
7    fmt::Debug,
8    rc::Rc,
9};
10
11use freya_engine::prelude::{
12    Canvas,
13    FontCollection,
14    FontMgr,
15    SkRRect,
16    SkRect,
17};
18use rustc_hash::FxHashMap;
19use torin::{
20    prelude::{
21        Area,
22        LayoutNode,
23        PostMeasure,
24        Size2D,
25    },
26    scaled::Scaled,
27    torin::Torin,
28};
29
30use crate::{
31    data::{
32        AccessibilityData,
33        EffectData,
34        LayoutData,
35        StyleState,
36        TextStyleData,
37        TextStyleState,
38    },
39    diff_key::DiffKey,
40    event_handler::EventHandler,
41    events::{
42        data::{
43            AccessibilityActionEventData,
44            Event,
45            KeyboardEventData,
46            MouseEventData,
47            PointerEventData,
48            SizedEventData,
49            StyledEventData,
50            TouchEventData,
51            VisibleEventData,
52            WheelEventData,
53        },
54        name::EventName,
55    },
56    layers::Layer,
57    node_id::NodeId,
58    prelude::{
59        Color,
60        FileEventData,
61        ImePreeditEventData,
62        MaybeExt,
63    },
64    style::fill::Fill,
65    text_cache::TextCache,
66    tree::{
67        DiffModifies,
68        Tree,
69    },
70};
71
72/// Tells Freya how to lay out, handle events for, describe accessibility and draw an element.
73///
74/// To make your own element, implement this trait and override what you need. For example,
75/// [`render`](Self::render) draws it, [`layout`](Self::layout) supplies its layout,
76/// [`measure`](Self::measure) gives it a custom size, and [`is_point_inside`](Self::is_point_inside)
77/// changes how pointer hits are detected. Everything has a default, so you can start with just
78/// the methods your element needs. Override [`changed`](Self::changed) and [`diff`](Self::diff)
79/// when changes to its data need to update the UI.
80///
81/// To put it in the UI tree, implement `From<YourElement> for Element` (or otherwise make it
82/// convertible into [`Element`]). You can also implement builder traits like `LayoutExt` to let
83/// callers set its properties. Freya stores elements as `Rc<dyn ElementExt>`, so they must be
84/// `'static` through the [`Any`].
85///
86/// See [`feature_element.rs`](https://github.com/marc2332/freya/blob/main/examples/feature_element.rs)
87/// for a full example.
88pub trait ElementExt: Any {
89    /// Converts this value into an [`Element`] if it implements `Into<Element>`.
90    fn into_element(self) -> Element
91    where
92        Self: Sized + Into<Element>,
93    {
94        self.into()
95    }
96
97    /// Returns whether this element has changed compared to another one.
98    fn changed(&self, _other: &Rc<dyn ElementExt>) -> bool {
99        false
100    }
101
102    /// Tells Freya what needs updating when this element changes.
103    fn diff(&self, _other: &Rc<dyn ElementExt>) -> DiffModifies {
104        DiffModifies::empty()
105    }
106
107    /// Returns layout properties used to position and size this element.
108    fn layout(&'_ self) -> Cow<'_, LayoutData> {
109        Cow::Owned(Default::default())
110    }
111
112    /// Returns accessibility properties for this element.
113    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
114        Cow::Owned(Default::default())
115    }
116
117    /// Returns optional effect properties for this element.
118    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
119        None
120    }
121
122    /// Returns visual style properties for this element.
123    fn style(&'_ self) -> Cow<'_, StyleState> {
124        Cow::Owned(Default::default())
125    }
126
127    /// Whether the element paints nothing, letting events fall through to
128    /// non-ancestor elements behind it.
129    fn is_transparent(&self) -> bool {
130        self.style().background == Fill::Color(Color::TRANSPARENT)
131    }
132
133    /// Returns text styling inherited by text content in this element.
134    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
135        Cow::Owned(Default::default())
136    }
137
138    /// Returns the drawing layer assigned to this element.
139    fn layer(&self) -> Layer {
140        Layer::default()
141    }
142
143    /// Returns event handlers registered on this element.
144    fn events_handlers(&'_ self) -> Option<Cow<'_, EventHandlers>> {
145        None
146    }
147
148    /// Measures the element and optionally returns measurement data for later layout stages.
149    fn measure(&self, _context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
150        None
151    }
152
153    /// Returns whether this element hooks into layout measurement.
154    fn should_hook_measurement(&self) -> bool {
155        false
156    }
157
158    /// Returns whether to measure this element's children.
159    fn should_measure_inner_children(&self) -> bool {
160        true
161    }
162
163    /// Whether this element needs a [ElementExt::post_measure] step after the layout pass.
164    fn needs_post_measure(&self) -> bool {
165        false
166    }
167
168    /// Runs after this element and its children are measured.
169    fn post_measure(&self, _context: PostMeasureContext) -> PostMeasure<NodeId> {
170        PostMeasure::default()
171    }
172
173    /// Tests whether the pointer position is inside this element's hit area.
174    fn is_point_inside(&self, context: EventMeasurementContext) -> bool {
175        context
176            .layout_node
177            .visible_area()
178            .contains(context.cursor.to_f32())
179    }
180
181    /// Applies clipping for this element before it and its children are rendered.
182    fn clip(&self, _context: ClipContext) {}
183
184    /// Renders this element using the supplied canvas and layout context.
185    fn render(&self, _context: RenderContext) {}
186
187    /// Creates a rounded rectangle from an area using this element's corner radius.
188    fn render_rect(&self, area: &Area, scale_factor: f32) -> SkRRect {
189        let style = self.style();
190        let corner_radius = style.corner_radius.with_scale(scale_factor);
191        SkRRect::new_rect_radii(
192            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
193            &[
194                (corner_radius.top_left(), corner_radius.top_left()).into(),
195                (corner_radius.top_right(), corner_radius.top_right()).into(),
196                (corner_radius.bottom_right(), corner_radius.bottom_right()).into(),
197                (corner_radius.bottom_left(), corner_radius.bottom_left()).into(),
198            ],
199        )
200    }
201
202    /// Mutates the accessibility node immediately before it enters the accessibility tree.
203    fn finish_accessibility(&self, _builder: &mut accesskit::Node) {}
204}
205
206#[allow(dead_code)]
207pub struct LayoutContext<'a> {
208    pub node_id: NodeId,
209    pub torin_node: &'a torin::node::Node,
210    pub area_size: &'a Size2D,
211    pub font_collection: &'a mut FontCollection,
212    pub font_manager: &'a FontMgr,
213    pub text_style_state: &'a TextStyleState,
214    pub fallback_fonts: &'a [Cow<'static, str>],
215    pub scale_factor: f64,
216    pub text_cache: &'a mut TextCache,
217}
218
219#[allow(dead_code)]
220pub struct RenderContext<'a> {
221    pub font_collection: &'a mut FontCollection,
222    pub canvas: &'a Canvas,
223    pub layout_node: &'a LayoutNode,
224    pub text_style_state: &'a TextStyleState,
225    pub tree: &'a Tree,
226    pub scale_factor: f64,
227}
228
229pub struct EventMeasurementContext<'a> {
230    pub cursor: ragnarok::CursorPoint,
231    pub layout_node: &'a LayoutNode,
232    pub scale_factor: f64,
233}
234
235pub struct PostMeasureContext<'a> {
236    pub node_layout: &'a LayoutNode,
237    pub children: &'a [NodeId],
238    pub layout: &'a Torin<NodeId>,
239    pub font_collection: &'a mut FontCollection,
240    pub text_style_state: &'a TextStyleState,
241    pub fallback_fonts: &'a [Cow<'static, str>],
242    pub scale_factor: f64,
243}
244
245pub struct ClipContext<'a> {
246    pub canvas: &'a Canvas,
247    pub visible_area: &'a Area,
248    pub scale_factor: f64,
249}
250
251impl<T: Any + PartialEq> ComponentProps for T {
252    fn changed(&self, other: &dyn ComponentProps) -> bool {
253        (other as &dyn Any)
254            .downcast_ref::<T>()
255            .is_none_or(|other| self != other)
256    }
257}
258
259pub trait ComponentProps: Any {
260    fn changed(&self, other: &dyn ComponentProps) -> bool;
261}
262
263#[derive(Clone)]
264pub enum Element {
265    Component {
266        key: DiffKey,
267        comp: Rc<dyn Fn(Rc<dyn ComponentProps>) -> Element>,
268        props: Rc<dyn ComponentProps>,
269    },
270    Element {
271        key: DiffKey,
272        element: Rc<dyn ElementExt>,
273        elements: Vec<Element>,
274    },
275}
276
277impl Debug for Element {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        match self {
280            Self::Element { key, elements, .. } => {
281                f.write_str(&format!("Element {{ key: {:?} }}", key))?;
282                elements.fmt(f)
283            }
284            Self::Component { key, .. } => f.write_str(&format!("Component {{ key: {:?} }}", key)),
285        }
286    }
287}
288
289pub trait IntoElement {
290    fn into_element(self) -> Element;
291}
292
293impl<T: Into<Element>> IntoElement for T {
294    fn into_element(self) -> Element {
295        self.into()
296    }
297}
298
299/// [App] is a trait for root-level application components.
300/// Types implementing [App] automatically implement [Component] and have a
301/// blanket [PartialEq] implementation that always returns true.
302pub trait App: 'static {
303    fn render(&self) -> impl IntoElement;
304}
305
306/// [AppComponent] is a wrapper for [App] components that returns true in equality checks.
307#[derive(Clone)]
308pub struct AppComponent {
309    render: Rc<dyn Fn() -> Element + 'static>,
310}
311
312impl AppComponent {
313    pub fn new(render: impl App + 'static) -> Self {
314        Self {
315            render: Rc::new(move || render.render().into_element()),
316        }
317    }
318}
319
320impl PartialEq for AppComponent {
321    fn eq(&self, _other: &Self) -> bool {
322        true
323    }
324}
325
326#[cfg(feature = "hotreload")]
327impl<F, E> From<F> for AppComponent
328where
329    F: Fn() -> E + Clone + 'static,
330    E: IntoElement,
331{
332    fn from(render: F) -> Self {
333        AppComponent {
334            render: Rc::new(move || {
335                crate::hotreload::subsecond::HotFn::current(render.clone())
336                    .call(())
337                    .into_element()
338            }),
339        }
340    }
341}
342
343#[cfg(not(feature = "hotreload"))]
344impl<F, E> From<F> for AppComponent
345where
346    F: Fn() -> E + 'static,
347    E: IntoElement,
348{
349    fn from(render: F) -> Self {
350        AppComponent {
351            render: Rc::new(move || render().into_element()),
352        }
353    }
354}
355
356impl Component for AppComponent {
357    fn render(&self) -> impl IntoElement {
358        (self.render)()
359    }
360}
361
362/// Encapsulate reusable pieces of UI by using the [Component] trait.
363/// Every [Component] creates a new layer of state in the app,
364/// meaning that implementors of [Component] can make use of hooks in their [Component::render] method.
365/// ```rust, no_run
366/// # use freya::prelude::*;
367/// #[derive(PartialEq)]
368/// struct ReusableCounter {
369///     pub init_number: u8,
370/// }
371///
372/// impl Component for ReusableCounter {
373///     fn render(&self) -> impl IntoElement {
374///         let mut number = use_state(|| self.init_number);
375///         label()
376///             .on_press(move |_| {
377///                 *number.write() += 1;
378///             })
379///             .text(number.read().to_string())
380///     }
381/// }
382/// ```
383pub trait Component: ComponentKey + PartialEq + 'static {
384    fn render(&self) -> impl IntoElement;
385
386    fn render_key(&self) -> DiffKey {
387        self.default_key()
388    }
389}
390
391pub trait ComponentOwned: ComponentKey + PartialEq + 'static {
392    fn render(self) -> impl IntoElement;
393
394    fn render_key(&self) -> DiffKey {
395        self.default_key()
396    }
397}
398
399pub trait ComponentKey {
400    fn default_key(&self) -> DiffKey;
401}
402
403impl<T> Component for T
404where
405    T: ComponentOwned + Clone + PartialEq,
406{
407    fn render(&self) -> impl IntoElement {
408        <Self as ComponentOwned>::render(self.clone())
409    }
410    fn render_key(&self) -> DiffKey {
411        <Self as ComponentOwned>::render_key(self)
412    }
413}
414
415impl<T> ComponentKey for T
416where
417    T: Component,
418{
419    fn default_key(&self) -> DiffKey {
420        use std::hash::{
421            Hash,
422            Hasher,
423        };
424        let mut hasher = rustc_hash::FxHasher::default();
425        TypeId::of::<T>().hash(&mut hasher);
426        DiffKey::DefaultU64(hasher.finish())
427    }
428}
429
430impl<T> MaybeExt for T where T: Component {}
431
432impl<T: Component> From<T> for Element {
433    fn from(value: T) -> Self {
434        let key = value.render_key();
435        Element::Component {
436            key,
437            #[cfg(feature = "hotreload")]
438            comp: Rc::new(move |props| {
439                let props = (&*props as &dyn Any).downcast_ref::<T>().unwrap();
440                crate::hotreload::subsecond::HotFn::current(|v: &T| v.render().into_element())
441                    .call((props,))
442            }),
443            #[cfg(not(feature = "hotreload"))]
444            comp: Rc::new(move |props| {
445                let props = (&*props as &dyn Any).downcast_ref::<T>().unwrap();
446                props.render().into_element()
447            }),
448            props: Rc::new(value),
449        }
450    }
451}
452
453impl PartialEq for Element {
454    fn eq(&self, other: &Self) -> bool {
455        match (self, other) {
456            (
457                Self::Component {
458                    key: key1,
459                    props: props1,
460                    ..
461                },
462                Self::Component {
463                    key: key2,
464                    props: props2,
465                    ..
466                },
467            ) => key1 == key2 && !props1.changed(props2.as_ref()),
468            (
469                Self::Element {
470                    key: key1,
471                    element: element1,
472                    elements: elements1,
473                },
474                Self::Element {
475                    key: key2,
476                    element: element2,
477                    elements: elements2,
478                },
479            ) => key1 == key2 && !element1.changed(element2) && elements1 == elements2,
480            _ => false,
481        }
482    }
483}
484
485pub type EventHandlers = FxHashMap<EventName, EventHandlerType>;
486
487#[derive(Clone, PartialEq)]
488pub enum EventHandlerType {
489    AccessibilityAction(EventHandler<Event<AccessibilityActionEventData>>),
490    Mouse(EventHandler<Event<MouseEventData>>),
491    Keyboard(EventHandler<Event<KeyboardEventData>>),
492    Sized(EventHandler<Event<SizedEventData>>),
493    Visible(EventHandler<Event<VisibleEventData>>),
494    Styled(EventHandler<Event<StyledEventData>>),
495    Wheel(EventHandler<Event<WheelEventData>>),
496    Touch(EventHandler<Event<TouchEventData>>),
497    Pointer(EventHandler<Event<PointerEventData>>),
498    ImePreedit(EventHandler<Event<ImePreeditEventData>>),
499    File(EventHandler<Event<FileEventData>>),
500}