1use std::{cell::RefCell, rc::Rc};
4
5use cranpose_core::{NodeId, SlotId};
6use cranpose_ui_graphics::Size;
7use cranpose_ui_layout::{MeasurePolicy, Placement};
8
9use super::{nodes::LayoutNode, scopes::BoxWithConstraintsScopeImpl};
10use crate::{
11 composable,
12 modifier::Modifier,
13 subcompose_layout::{
14 Constraints, MeasurePolicy as SubcomposeMeasurePolicy, MeasureResult, SubcomposeLayoutNode,
15 SubcomposeLayoutScope, SubcomposeMeasureScope, SubcomposeMeasureScopeImpl,
16 },
17};
18
19pub(crate) fn caller_modifier_changed(modifier: &Modifier) -> bool {
20 cranpose_core::remember(|| RefCell::new(modifier.clone())).with(|last| {
21 let changed = *last.borrow() != *modifier;
22 if changed {
23 *last.borrow_mut() = modifier.clone();
24 }
25 changed
26 })
27}
28
29#[derive(Clone, Copy)]
33pub(crate) struct LayoutEnvironment {
34 pub(crate) density: crate::density::Density,
35 pub(crate) direction: crate::layout_direction::LayoutDirection,
36}
37
38impl LayoutEnvironment {
39 pub(crate) fn current() -> Self {
41 Self {
42 density: crate::density::density(),
43 direction: crate::layout_direction(),
44 }
45 }
46}
47
48#[composable(no_skip)]
53pub fn Layout<F, P>(modifier: Modifier, measure_policy: P, content: F) -> NodeId
54where
55 F: FnMut() + 'static,
56 P: MeasurePolicy + Clone + PartialEq + 'static,
57{
58 compose_layout(
59 modifier,
60 &measure_policy,
61 LayoutEnvironment::current(),
62 content,
63 )
64}
65
66pub(crate) fn compose_layout<F, P>(
69 modifier: Modifier,
70 measure_policy: &P,
71 environment: LayoutEnvironment,
72 mut content: F,
73) -> NodeId
74where
75 F: FnMut() + 'static,
76 P: MeasurePolicy + Clone + PartialEq,
77{
78 cranpose_core::with_current_composer(|composer| {
81 let (id, provided, _) = emit_layout_node(composer, modifier, measure_policy, environment);
82 composer.push_parent(id);
83 compose_under_locals(provided, &mut content);
84 composer.pop_parent();
85 id
86 })
87}
88
89pub(crate) fn compose_empty_layout(modifier: Modifier, environment: LayoutEnvironment) -> NodeId {
90 cranpose_core::with_current_composer(|composer| {
91 let (id, provided, holds_children) = emit_layout_node(
92 composer,
93 modifier,
94 &crate::layout::policies::EmptyMeasurePolicy,
95 environment,
96 );
97 if provided.is_empty() {
98 composer.compose_no_children(id, holds_children);
99 } else {
100 composer.push_parent(id);
101 compose_under_locals(provided, &mut || {});
102 composer.pop_parent();
103 }
104 id
105 })
106}
107
108trait PolicyUpdate {
111 fn new_node(&self, modifier: Modifier) -> LayoutNode;
113 fn update(&self, node: &mut LayoutNode);
115}
116
117impl<P: MeasurePolicy + Clone + PartialEq> PolicyUpdate for P {
118 fn new_node(&self, modifier: Modifier) -> LayoutNode {
119 LayoutNode::new(modifier, held_policy(self))
120 }
121
122 fn update(&self, node: &mut LayoutNode) {
123 let current: &dyn std::any::Any = &*node.measure_policy;
124 if current.downcast_ref::<P>() != Some(self) {
125 node.set_measure_policy(held_policy(self));
126 }
127 }
128}
129
130fn held_policy<P: MeasurePolicy + Clone>(policy: &P) -> Rc<dyn MeasurePolicy> {
133 if std::any::TypeId::of::<P>()
134 == std::any::TypeId::of::<crate::layout::policies::EmptyMeasurePolicy>()
135 {
136 return crate::widgets::nodes::layout_node::empty_measure_policy();
137 }
138 Rc::new(policy.clone())
139}
140
141#[inline(never)]
146fn emit_layout_node(
147 composer: &cranpose_core::Composer,
148 modifier: Modifier,
149 policy: &dyn PolicyUpdate,
150 environment: LayoutEnvironment,
151) -> (NodeId, Vec<cranpose_core::ProvidedValue>, bool) {
152 let provided = modifier.provided_composition_locals();
153 let id = composer.emit_recyclable_node(
154 || policy.new_node(modifier.clone()),
155 |node| *node = policy.new_node(modifier.clone()),
156 );
157 let holds_children = composer
158 .with_node_mut(id, |node: &mut LayoutNode| {
159 node.set_modifier(modifier);
160 policy.update(node);
161 node.set_density(environment.density);
162 node.set_layout_direction(environment.direction);
163 !node.children.is_empty()
164 })
165 .unwrap_or_else(|err| {
166 debug_assert!(false, "failed to update Layout node: {err}");
167 true
168 });
169 (id, provided, holds_children)
170}
171
172fn compose_under_locals(provided: Vec<cranpose_core::ProvidedValue>, content: &mut dyn FnMut()) {
173 if provided.is_empty() {
174 content();
175 return;
176 }
177 cranpose_core::CompositionLocalProvider(provided, content);
178}
179
180#[composable]
183pub fn SubcomposeLayout(
184 modifier: Modifier,
185 measure_policy: impl for<'scope> Fn(
186 &mut SubcomposeMeasureScopeImpl<'scope>,
187 Constraints,
188 ) -> MeasureResult
189 + 'static,
190) -> NodeId {
191 cranpose_core::debug_label_current_scope("SubcomposeLayout");
192 let policy_cell =
193 cranpose_core::remember(|| Rc::new(RefCell::new(None::<Rc<SubcomposeMeasurePolicy>>)))
194 .with(|cell| Rc::clone(cell));
195 let current_policy: Rc<SubcomposeMeasurePolicy> = Rc::new(measure_policy);
196 let policy_captures_changed = {
197 let mut policy_cell_ref = policy_cell.borrow_mut();
198 let changed = policy_cell_ref
199 .as_ref()
200 .is_none_or(|previous| !Rc::ptr_eq(previous, ¤t_policy));
201 *policy_cell_ref = Some(current_policy);
202 changed
203 };
204 let policy: Rc<SubcomposeMeasurePolicy> = cranpose_core::remember(move || {
205 let policy_cell = Rc::clone(&policy_cell);
206 let policy: Rc<SubcomposeMeasurePolicy> =
207 Rc::new(
208 move |scope, constraints| match policy_cell.borrow().as_ref().cloned() {
209 Some(current) => current(scope, constraints),
210 None => empty_subcompose_measure_result(constraints),
211 },
212 );
213 policy
214 })
215 .with(|policy| Rc::clone(policy));
216 let id = cranpose_core::with_current_composer(|composer| {
217 composer.emit_node(|| SubcomposeLayoutNode::new(modifier.clone(), Rc::clone(&policy)))
218 });
219 let captured_context =
220 cranpose_core::with_current_composer(|composer| composer.capture_composition_context());
221 let composed_density = crate::density::density();
222 let direction = crate::layout_direction();
223 if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut SubcomposeLayoutNode| {
224 node.set_modifier(modifier);
225 node.set_measure_policy(policy);
226 node.set_captured_context(captured_context);
227 node.set_density(composed_density);
228 node.set_layout_direction(direction);
229 if policy_captures_changed {
230 node.invalidate_subcomposition();
231 }
232 }) {
233 debug_assert!(false, "failed to update SubcomposeLayout node: {err}");
234 }
235 id
236}
237
238fn empty_subcompose_measure_result(constraints: Constraints) -> MeasureResult {
239 let (width, height) = constraints.constrain(0.0, 0.0);
240 MeasureResult::new(Size { width, height }, Vec::new())
241}
242
243#[composable(no_skip)]
246pub fn BoxWithConstraints<F>(modifier: Modifier, content: F) -> NodeId
247where
248 F: FnMut(BoxWithConstraintsScopeImpl) + 'static,
249{
250 let content_ref: Rc<RefCell<F>> = Rc::new(RefCell::new(content));
251 SubcomposeLayout(modifier, move |scope, constraints| {
252 let scope_impl = BoxWithConstraintsScopeImpl::new(constraints);
253 let scope_for_content = scope_impl;
254 let measurables = {
255 let content_ref = Rc::clone(&content_ref);
256 scope.subcompose(SlotId::new(0), constraints, move || {
257 cranpose_core::debug_label_current_scope("BoxWithConstraints.slot(0)");
258 let mut content = content_ref.borrow_mut();
259 content(scope_for_content);
260 })
261 };
262 let child_constraints = Constraints {
263 min_width: 0.0,
264 max_width: constraints.max_width,
265 min_height: 0.0,
266 max_height: constraints.max_height,
267 };
268
269 let mut width = 0.0_f32;
270 let mut height = 0.0_f32;
271 let mut placements = Vec::with_capacity(measurables.len());
272
273 for measurable in measurables {
274 let placeable = scope.measure(measurable, child_constraints);
275 width = width.max(placeable.width());
276 height = height.max(placeable.height());
277 placeable.place(0.0, 0.0);
278 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0.0));
279 }
280
281 width = width.clamp(constraints.min_width, constraints.max_width);
282 height = height.clamp(constraints.min_height, constraints.max_height);
283 scope.layout(width, height, placements)
284 })
285}
286
287#[cfg(test)]
288#[path = "tests/layout_tests.rs"]
289mod tests;