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