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