feather_ui/layout/
leaf.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use super::base::Empty;
5use super::{Concrete, Desc, Layout, Renderable, Staged, base, map_unsized_area};
6use crate::{DRect, PxDim, PxRect, SourceID, rtree};
7use std::marker::PhantomData;
8use std::rc::Rc;
9
10pub trait Prop: base::Area + base::Limits + base::Anchor {}
11
12crate::gen_from_to_dyn!(Prop);
13
14impl Prop for DRect {}
15
16// Actual leaves do not require padding, but a lot of raw elements do (text,
17// shape, images, etc.) This inherits Prop to allow elements to "extract" the
18// padding for the rendering system for when it doesn't affect layouts.
19pub trait Padded: Prop + base::Padding {}
20
21crate::gen_from_to_dyn!(Padded);
22
23impl Padded for DRect {}
24
25impl Desc for dyn Prop {
26    type Props = dyn Prop;
27    type Child = dyn Empty;
28    type Children = PhantomData<dyn Layout<Self::Child>>;
29
30    fn stage<'a>(
31        props: &Self::Props,
32        outer_area: PxRect,
33        outer_limits: crate::PxLimits,
34        _: &Self::Children,
35        id: std::sync::Weak<SourceID>,
36        renderable: Option<Rc<dyn Renderable>>,
37        window: &mut crate::component::window::WindowState,
38    ) -> Box<dyn Staged + 'a> {
39        let limits = outer_limits + props.limits().resolve(window.dpi);
40        let evaluated_area = super::limit_area(
41            map_unsized_area(props.area().resolve(window.dpi), PxDim::zero())
42                * super::nuetralize_unsized(outer_area),
43            limits,
44        );
45
46        let anchor = props.anchor().resolve(window.dpi) * evaluated_area.dim();
47        let evaluated_area = evaluated_area - anchor;
48
49        debug_assert!(evaluated_area.v.is_finite().all());
50        Box::new(Concrete {
51            area: evaluated_area,
52            renderable,
53            rtree: rtree::Node::new(
54                evaluated_area.to_untyped(),
55                None,
56                Default::default(),
57                id,
58                window,
59            ),
60            children: Default::default(),
61            layer: None,
62        })
63    }
64}
65
66/// A sized leaf is one with inherent size, like an image. This is used to
67/// preserve aspect ratio when encounting an unsized axis. This must be provided
68/// in pixels.
69
70#[derive_where::derive_where(Clone)]
71pub struct Sized<T> {
72    pub id: std::sync::Weak<SourceID>,
73    pub props: Rc<T>,
74    pub size: crate::PxDim,
75    pub renderable: Option<Rc<dyn Renderable>>,
76}
77
78impl<T: Padded> Layout<T> for Sized<T> {
79    fn get_props(&self) -> &T {
80        &self.props
81    }
82    fn stage<'a>(
83        &self,
84        outer_area: crate::PxRect,
85        outer_limits: crate::PxLimits,
86        window: &mut crate::component::window::WindowState,
87    ) -> Box<dyn super::Staged + 'a> {
88        let limits = outer_limits + self.props.limits().resolve(window.dpi);
89        let padding = self.props.padding().as_perimeter(window.dpi);
90        let area = self.props.area().resolve(window.dpi);
91        let aspect_ratio = self.size.width / self.size.height; // Will be NAN if both are 0, which disables any attempt to preserve aspect ratio
92
93        // The way we handle unsized here is different from how we normally handle it.
94        // If both axes are unsized, we simply set the area to the internal
95        // size. If only one axis is unsized, we stretch it to maintain an aspect
96        // ratio relative to the size of the other axis.
97        let (unsized_x, unsized_y) = super::check_unsized(area);
98        let outer_area = super::nuetralize_unsized(outer_area);
99        let mapped_area = match (unsized_x, unsized_y, aspect_ratio.is_finite()) {
100            (true, false, false) => {
101                let mut presize = map_unsized_area(area, PxDim::zero()) * outer_area;
102                let adjust = presize.dim().height * aspect_ratio;
103                let v = presize.v.as_array_mut();
104                v[2] += adjust;
105                presize
106            }
107            (false, true, false) => {
108                let mut presize = map_unsized_area(area, PxDim::zero()) * outer_area;
109                // Be careful, the aspect ratio here is being divided instead of multiplied
110                let adjust = presize.dim().width / aspect_ratio;
111                let v = presize.v.as_array_mut();
112                v[3] += adjust;
113                presize
114            }
115            _ => {
116                map_unsized_area(area, self.size + padding.topleft() + padding.bottomright())
117                    * outer_area
118            }
119        };
120
121        let evaluated_area = super::limit_area(mapped_area, limits);
122
123        let anchor = self.props.anchor().resolve(window.dpi) * evaluated_area.dim();
124        let evaluated_area = evaluated_area - anchor;
125
126        debug_assert!(
127            evaluated_area.v.is_finite().all(),
128            "non-finite evaluated area!"
129        );
130        debug_assert!(evaluated_area.v.is_finite().all());
131        Box::new(Concrete {
132            area: evaluated_area,
133            renderable: self.renderable.clone(),
134            rtree: rtree::Node::new(
135                evaluated_area.to_untyped(),
136                None,
137                Default::default(),
138                self.id.clone(),
139                window,
140            ),
141            children: Default::default(),
142            layer: None,
143        })
144    }
145}