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.replace(modifier.clone());
24 }
25 changed
26 })
27}
28
29struct RetainedMeasurePolicy<P> {
30 value: P,
31 policy: Rc<dyn MeasurePolicy>,
32}
33
34#[composable]
35pub fn Layout<F, P>(modifier: Modifier, measure_policy: P, content: F) -> NodeId
36where
37 F: FnMut() + 'static,
38 P: MeasurePolicy + Clone + PartialEq + 'static,
39{
40 compose_layout(modifier, measure_policy, crate::density::density(), content)
41}
42
43pub(crate) fn compose_layout<F, P>(
46 modifier: Modifier,
47 measure_policy: P,
48 composed_density: crate::density::Density,
49 content: F,
50) -> NodeId
51where
52 F: FnMut() + 'static,
53 P: MeasurePolicy + Clone + PartialEq + 'static,
54{
55 let policy_holder = cranpose_core::remember({
56 let measure_policy = measure_policy.clone();
57 move || {
58 Rc::new(RefCell::new(RetainedMeasurePolicy {
59 value: measure_policy.clone(),
60 policy: Rc::new(measure_policy),
61 }))
62 }
63 })
64 .with(Rc::clone);
65 let policy = {
66 let mut holder = policy_holder.borrow_mut();
67 if holder.value != measure_policy {
68 holder.value = measure_policy.clone();
69 holder.policy = Rc::new(measure_policy);
70 }
71 Rc::clone(&holder.policy)
72 };
73 emit_layout(modifier, policy, composed_density, content)
74}
75
76pub(crate) fn compose_empty_layout(
77 modifier: Modifier,
78 composed_density: crate::density::Density,
79) -> NodeId {
80 thread_local! {
81 static EMPTY: Rc<dyn MeasurePolicy> = Rc::new(crate::layout::policies::EmptyMeasurePolicy);
82 }
83 emit_layout(modifier, EMPTY.with(Rc::clone), composed_density, || {})
84}
85
86fn emit_layout<F>(
87 modifier: Modifier,
88 policy: Rc<dyn MeasurePolicy>,
89 composed_density: crate::density::Density,
90 mut content: F,
91) -> NodeId
92where
93 F: FnMut() + 'static,
94{
95 let id = cranpose_core::with_current_composer(|composer| {
96 composer.emit_recyclable_node(
97 || LayoutNode::new(modifier.clone(), Rc::clone(&policy)),
98 |node| *node = LayoutNode::new(modifier.clone(), Rc::clone(&policy)),
99 )
100 });
101 let provided = modifier.provided_composition_locals();
102 let direction = crate::layout_direction();
103 if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut LayoutNode| {
104 node.set_modifier(modifier);
105 node.set_measure_policy(policy);
106 node.set_density(composed_density);
107 node.set_layout_direction(direction);
108 }) {
109 debug_assert!(false, "failed to update Layout node: {err}");
110 }
111 cranpose_core::push_parent(id);
112 compose_under_locals(provided, &mut content);
113 cranpose_core::pop_parent();
114 id
115}
116
117fn compose_under_locals(provided: Vec<cranpose_core::ProvidedValue>, content: &mut dyn FnMut()) {
118 if provided.is_empty() {
119 content();
120 return;
121 }
122 cranpose_core::CompositionLocalProvider(provided, content);
123}
124
125#[composable]
126pub fn SubcomposeLayout(
127 modifier: Modifier,
128 measure_policy: impl for<'scope> Fn(
129 &mut SubcomposeMeasureScopeImpl<'scope>,
130 Constraints,
131 ) -> MeasureResult
132 + 'static,
133) -> NodeId {
134 cranpose_core::debug_label_current_scope("SubcomposeLayout");
135 let policy_cell =
136 cranpose_core::remember(|| Rc::new(RefCell::new(None::<Rc<SubcomposeMeasurePolicy>>)))
137 .with(|cell| cell.clone());
138 let current_policy: Rc<SubcomposeMeasurePolicy> = Rc::new(measure_policy);
139 let policy_captures_changed = {
140 let mut policy_cell_ref = policy_cell.borrow_mut();
141 let changed = policy_cell_ref
142 .as_ref()
143 .is_none_or(|previous| !Rc::ptr_eq(previous, ¤t_policy));
144 *policy_cell_ref = Some(current_policy);
145 changed
146 };
147 let policy: Rc<SubcomposeMeasurePolicy> = cranpose_core::remember(move || {
148 let policy_cell = policy_cell.clone();
149 let policy: Rc<SubcomposeMeasurePolicy> =
150 Rc::new(
151 move |scope, constraints| match policy_cell.borrow().as_ref().cloned() {
152 Some(current) => current(scope, constraints),
153 None => empty_subcompose_measure_result(constraints),
154 },
155 );
156 policy
157 })
158 .with(|policy| policy.clone());
159 let id = cranpose_core::with_current_composer(|composer| {
160 composer.emit_node(|| SubcomposeLayoutNode::new(modifier.clone(), Rc::clone(&policy)))
161 });
162 let captured_context =
163 cranpose_core::with_current_composer(|composer| composer.capture_composition_context());
164 let composed_density = crate::density::density();
165 let direction = crate::layout_direction();
166 if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut SubcomposeLayoutNode| {
167 node.set_modifier(modifier);
168 node.set_measure_policy(policy);
169 node.set_captured_context(captured_context);
170 node.set_density(composed_density);
171 node.set_layout_direction(direction);
172 if policy_captures_changed {
173 node.invalidate_subcomposition();
174 }
175 }) {
176 debug_assert!(false, "failed to update SubcomposeLayout node: {err}");
177 }
178 id
179}
180
181fn empty_subcompose_measure_result(constraints: Constraints) -> MeasureResult {
182 let (width, height) = constraints.constrain(0.0, 0.0);
183 MeasureResult::new(Size { width, height }, Vec::new())
184}
185
186#[composable(no_skip)]
187pub fn BoxWithConstraints<F>(modifier: Modifier, content: F) -> NodeId
188where
189 F: FnMut(BoxWithConstraintsScopeImpl) + 'static,
190{
191 let content_ref: Rc<RefCell<F>> = Rc::new(RefCell::new(content));
192 SubcomposeLayout(modifier, move |scope, constraints| {
193 let scope_impl = BoxWithConstraintsScopeImpl::new(constraints);
194 let scope_for_content = scope_impl;
195 let measurables = {
196 let content_ref = Rc::clone(&content_ref);
197 scope.subcompose(SlotId::new(0), constraints, move || {
198 cranpose_core::debug_label_current_scope("BoxWithConstraints.slot(0)");
199 let mut content = content_ref.borrow_mut();
200 content(scope_for_content);
201 })
202 };
203 let child_constraints = Constraints {
204 min_width: 0.0,
205 max_width: constraints.max_width,
206 min_height: 0.0,
207 max_height: constraints.max_height,
208 };
209
210 let mut width = 0.0_f32;
211 let mut height = 0.0_f32;
212 let mut placements = Vec::with_capacity(measurables.len());
213
214 for measurable in measurables {
215 let placeable = scope.measure(measurable, child_constraints);
216 width = width.max(placeable.width());
217 height = height.max(placeable.height());
218 placeable.place(0.0, 0.0);
219 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
220 }
221
222 width = width.clamp(constraints.min_width, constraints.max_width);
223 height = height.clamp(constraints.min_height, constraints.max_height);
224 scope.layout(width, height, placements)
225 })
226}
227
228#[cfg(test)]
229#[path = "tests/layout_tests.rs"]
230mod tests;