Skip to main content

cranpose_ui/modifier/
wrap_content.rs

1//! Content measured at its own size along an axis, whatever minimum the parent
2//! sets there, and placed in the room it leaves by an alignment: Compose's
3//! `wrapContentWidth`, `wrapContentHeight` and `wrapContentSize`.
4
5use std::hash::{Hash, Hasher};
6
7use cranpose_foundation::{
8    Constraints, DelegatableNode, InvalidationKind, LayoutModifierNode, Measurable, ModifierNode,
9    ModifierNodeContext, ModifierNodeElement, NodeCapabilities, NodeState, Size,
10};
11use cranpose_ui_layout::{
12    Alignment, HorizontalAlignment, LayoutModifierMeasureResult, VerticalAlignment,
13};
14
15use super::{Modifier, inspector_metadata};
16
17/// The axes whose content is measured at its own size.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub(crate) enum WrapAxes {
20    /// The width only.
21    Horizontal,
22    /// The height only.
23    Vertical,
24    /// Both.
25    Both,
26}
27
28impl WrapAxes {
29    fn horizontal(self) -> bool {
30        self != WrapAxes::Vertical
31    }
32
33    fn vertical(self) -> bool {
34        self != WrapAxes::Horizontal
35    }
36}
37
38/// The constraints the content is measured with: no minimum on a wrapped
39/// axis, and no maximum there either when `unbounded`.
40pub(crate) fn wrap_content_constraints(
41    axes: WrapAxes,
42    unbounded: bool,
43    constraints: Constraints,
44) -> Constraints {
45    let (horizontal, vertical) = (axes.horizontal(), axes.vertical());
46    Constraints {
47        min_width: if horizontal {
48            0.0
49        } else {
50            constraints.min_width
51        },
52        max_width: if horizontal && unbounded {
53            f32::INFINITY
54        } else {
55            constraints.max_width
56        },
57        min_height: if vertical {
58            0.0
59        } else {
60            constraints.min_height
61        },
62        max_height: if vertical && unbounded {
63            f32::INFINITY
64        } else {
65            constraints.max_height
66        },
67    }
68}
69
70/// The size the wrapper takes, the content's coerced into `constraints`, and
71/// where the content sits in it: aligned on a wrapped axis by `alignment` on
72/// the device pixel grid of `density`, reaching outside the wrapper when it
73/// is larger.
74pub(crate) fn wrap_content_placement(
75    axes: WrapAxes,
76    alignment: Alignment,
77    constraints: Constraints,
78    content: Size,
79    density: f32,
80) -> LayoutModifierMeasureResult {
81    let width = content
82        .width
83        .max(constraints.min_width)
84        .min(constraints.max_width);
85    let height = content
86        .height
87        .max(constraints.min_height)
88        .min(constraints.max_height);
89    let x = if axes.horizontal() {
90        alignment.horizontal.align(width, content.width, density)
91    } else {
92        0.0
93    };
94    let y = if axes.vertical() {
95        alignment.vertical.align(height, content.height, density)
96    } else {
97        0.0
98    };
99    LayoutModifierMeasureResult::new(Size { width, height }, x, y)
100}
101
102/// The element behind [`Modifier::wrap_content_size`] and its one-axis forms.
103#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct WrapContentElement {
105    axes: WrapAxes,
106    alignment: Alignment,
107    unbounded: bool,
108}
109
110impl Hash for WrapContentElement {
111    fn hash<H: Hasher>(&self, state: &mut H) {
112        self.axes.hash(state);
113        self.alignment.horizontal.bias().to_bits().hash(state);
114        self.alignment.vertical.bias().to_bits().hash(state);
115        self.unbounded.hash(state);
116    }
117}
118
119/// The layout node that measures its content at its own size.
120pub struct WrapContentNode {
121    axes: WrapAxes,
122    alignment: Alignment,
123    unbounded: bool,
124    state: NodeState,
125}
126
127impl DelegatableNode for WrapContentNode {
128    fn node_state(&self) -> &NodeState {
129        &self.state
130    }
131}
132
133crate::modifier_nodes::impl_layout_modifier_node!(
134    WrapContentNode,
135    invalidate = InvalidationKind::Layout
136);
137
138impl LayoutModifierNode for WrapContentNode {
139    fn measure(
140        &self,
141        context: &mut dyn ModifierNodeContext,
142        measurable: &dyn Measurable,
143        constraints: Constraints,
144    ) -> LayoutModifierMeasureResult {
145        let placeable = measurable.measure(wrap_content_constraints(
146            self.axes,
147            self.unbounded,
148            constraints,
149        ));
150        wrap_content_placement(
151            self.axes,
152            self.alignment,
153            constraints,
154            Size {
155                width: placeable.width(),
156                height: placeable.height(),
157            },
158            context.density(),
159        )
160    }
161}
162
163impl ModifierNodeElement for WrapContentElement {
164    type Node = WrapContentNode;
165
166    fn create(&self) -> Self::Node {
167        WrapContentNode {
168            axes: self.axes,
169            alignment: self.alignment,
170            unbounded: self.unbounded,
171            state: NodeState::new(),
172        }
173    }
174
175    fn update(&self, node: &mut Self::Node) {
176        node.axes = self.axes;
177        node.alignment = self.alignment;
178        node.unbounded = self.unbounded;
179    }
180
181    fn capabilities(&self) -> NodeCapabilities {
182        NodeCapabilities::LAYOUT
183    }
184
185    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
186        Some(InvalidationKind::Layout)
187    }
188}
189
190impl Modifier {
191    /// Measures the content at its own width, whatever minimum width the
192    /// parent sets, and places it by `align` in the width the parent leaves.
193    /// With `unbounded` the content may also be wider than the parent's
194    /// maximum and reaches outside it. Compose's
195    /// `Modifier.wrapContentWidth(align, unbounded)`.
196    ///
197    /// Example: `Modifier::empty().fill_max_width().wrap_content_width(HorizontalAlignment::End, false)`
198    pub fn wrap_content_width(self, align: HorizontalAlignment, unbounded: bool) -> Self {
199        self.wrap_content(
200            WrapAxes::Horizontal,
201            Alignment::new(align, VerticalAlignment::CenterVertically),
202            unbounded,
203        )
204    }
205
206    /// Measures the content at its own height, whatever minimum height the
207    /// parent sets, and places it by `align` in the height the parent leaves.
208    /// With `unbounded` the content may also be taller than the parent's
209    /// maximum. Compose's `Modifier.wrapContentHeight(align, unbounded)`.
210    pub fn wrap_content_height(self, align: VerticalAlignment, unbounded: bool) -> Self {
211        self.wrap_content(
212            WrapAxes::Vertical,
213            Alignment::new(HorizontalAlignment::CenterHorizontally, align),
214            unbounded,
215        )
216    }
217
218    /// Measures the content at its own size on both axes and places it by
219    /// `align`. With `unbounded` the content may be larger than the parent
220    /// allows, as a fixed-size canvas scaled into a smaller window is.
221    /// Compose's `Modifier.wrapContentSize(align, unbounded)`.
222    ///
223    /// Example: `Modifier::empty().wrap_content_size(Alignment::TOP_START, true).size_points(1280.0, 820.0)`
224    pub fn wrap_content_size(self, align: Alignment, unbounded: bool) -> Self {
225        self.wrap_content(WrapAxes::Both, align, unbounded)
226    }
227
228    fn wrap_content(self, axes: WrapAxes, alignment: Alignment, unbounded: bool) -> Self {
229        let modifier = Self::with_element(WrapContentElement {
230            axes,
231            alignment,
232            unbounded,
233        })
234        .with_inspector_metadata(inspector_metadata("wrapContent", move |info| {
235            info.add_alignment("align", alignment);
236            info.add_property("unbounded", format!("{unbounded}"));
237        }));
238        self.then(modifier)
239    }
240}
241
242#[cfg(test)]
243#[path = "tests/wrap_content_tests.rs"]
244mod tests;