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