1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub(crate) enum WrapAxes {
20 Horizontal,
22 Vertical,
24 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
38pub(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
70pub(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#[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
119pub 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 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 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 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;