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#[derive(Clone, Copy)]
38pub(crate) struct LayoutEnvironment {
39 pub(crate) density: crate::density::Density,
40 pub(crate) direction: crate::layout_direction::LayoutDirection,
41}
42
43impl LayoutEnvironment {
44 pub(crate) fn current() -> Self {
46 Self {
47 density: crate::density::density(),
48 direction: crate::layout_direction(),
49 }
50 }
51}
52
53#[composable]
56pub fn Layout<F, P>(modifier: Modifier, measure_policy: P, content: F) -> NodeId
57where
58 F: FnMut() + 'static,
59 P: MeasurePolicy + Clone + PartialEq + 'static,
60{
61 compose_layout(
62 modifier,
63 measure_policy,
64 LayoutEnvironment::current(),
65 content,
66 )
67}
68
69pub(crate) fn compose_layout<F, P>(
72 modifier: Modifier,
73 measure_policy: P,
74 environment: LayoutEnvironment,
75 content: F,
76) -> NodeId
77where
78 F: FnMut() + 'static,
79 P: MeasurePolicy + Clone + PartialEq + 'static,
80{
81 let policy_holder = cranpose_core::remember({
82 let measure_policy = measure_policy.clone();
83 move || {
84 Rc::new(RefCell::new(RetainedMeasurePolicy {
85 value: measure_policy.clone(),
86 policy: Rc::new(measure_policy),
87 }))
88 }
89 })
90 .with(Rc::clone);
91 let policy = {
92 let mut holder = policy_holder.borrow_mut();
93 if holder.value != measure_policy {
94 holder.value = measure_policy.clone();
95 holder.policy = Rc::new(measure_policy);
96 }
97 Rc::clone(&holder.policy)
98 };
99 emit_layout(modifier, policy, environment, content)
100}
101
102pub(crate) fn compose_empty_layout(modifier: Modifier, environment: LayoutEnvironment) -> NodeId {
103 let (id, provided, holds_children) = emit_layout_node(
104 modifier,
105 super::nodes::layout_node::empty_measure_policy(),
106 environment,
107 );
108 if provided.is_empty() {
109 cranpose_core::with_current_composer(|composer| {
110 composer.compose_no_children(id, holds_children);
111 });
112 } else {
113 cranpose_core::push_parent(id);
114 compose_under_locals(provided, &mut || {});
115 cranpose_core::pop_parent();
116 }
117 id
118}
119
120fn emit_layout<F>(
121 modifier: Modifier,
122 policy: Rc<dyn MeasurePolicy>,
123 environment: LayoutEnvironment,
124 mut content: F,
125) -> NodeId
126where
127 F: FnMut() + 'static,
128{
129 let (id, provided, _) = emit_layout_node(modifier, policy, environment);
130 cranpose_core::push_parent(id);
131 compose_under_locals(provided, &mut content);
132 cranpose_core::pop_parent();
133 id
134}
135
136fn emit_layout_node(
139 modifier: Modifier,
140 policy: Rc<dyn MeasurePolicy>,
141 environment: LayoutEnvironment,
142) -> (NodeId, Vec<cranpose_core::ProvidedValue>, bool) {
143 let provided = modifier.provided_composition_locals();
144 let (id, holds_children) = cranpose_core::with_current_composer(|composer| {
145 let id = composer.emit_recyclable_node(
146 || LayoutNode::new(modifier.clone(), Rc::clone(&policy)),
147 |node| *node = LayoutNode::new(modifier.clone(), Rc::clone(&policy)),
148 );
149 let holds_children = composer
150 .with_node_mut(id, |node: &mut LayoutNode| {
151 node.set_modifier(modifier);
152 node.set_measure_policy(policy);
153 node.set_density(environment.density);
154 node.set_layout_direction(environment.direction);
155 !node.children.is_empty()
156 })
157 .unwrap_or_else(|err| {
158 debug_assert!(false, "failed to update Layout node: {err}");
159 true
160 });
161 (id, holds_children)
162 });
163 (id, provided, holds_children)
164}
165
166fn compose_under_locals(provided: Vec<cranpose_core::ProvidedValue>, content: &mut dyn FnMut()) {
167 if provided.is_empty() {
168 content();
169 return;
170 }
171 cranpose_core::CompositionLocalProvider(provided, content);
172}
173
174#[composable]
177pub fn SubcomposeLayout(
178 modifier: Modifier,
179 measure_policy: impl for<'scope> Fn(
180 &mut SubcomposeMeasureScopeImpl<'scope>,
181 Constraints,
182 ) -> MeasureResult
183 + 'static,
184) -> NodeId {
185 cranpose_core::debug_label_current_scope("SubcomposeLayout");
186 let policy_cell =
187 cranpose_core::remember(|| Rc::new(RefCell::new(None::<Rc<SubcomposeMeasurePolicy>>)))
188 .with(|cell| cell.clone());
189 let current_policy: Rc<SubcomposeMeasurePolicy> = Rc::new(measure_policy);
190 let policy_captures_changed = {
191 let mut policy_cell_ref = policy_cell.borrow_mut();
192 let changed = policy_cell_ref
193 .as_ref()
194 .is_none_or(|previous| !Rc::ptr_eq(previous, ¤t_policy));
195 *policy_cell_ref = Some(current_policy);
196 changed
197 };
198 let policy: Rc<SubcomposeMeasurePolicy> = cranpose_core::remember(move || {
199 let policy_cell = policy_cell.clone();
200 let policy: Rc<SubcomposeMeasurePolicy> =
201 Rc::new(
202 move |scope, constraints| match policy_cell.borrow().as_ref().cloned() {
203 Some(current) => current(scope, constraints),
204 None => empty_subcompose_measure_result(constraints),
205 },
206 );
207 policy
208 })
209 .with(|policy| policy.clone());
210 let id = cranpose_core::with_current_composer(|composer| {
211 composer.emit_node(|| SubcomposeLayoutNode::new(modifier.clone(), Rc::clone(&policy)))
212 });
213 let captured_context =
214 cranpose_core::with_current_composer(|composer| composer.capture_composition_context());
215 let composed_density = crate::density::density();
216 let direction = crate::layout_direction();
217 if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut SubcomposeLayoutNode| {
218 node.set_modifier(modifier);
219 node.set_measure_policy(policy);
220 node.set_captured_context(captured_context);
221 node.set_density(composed_density);
222 node.set_layout_direction(direction);
223 if policy_captures_changed {
224 node.invalidate_subcomposition();
225 }
226 }) {
227 debug_assert!(false, "failed to update SubcomposeLayout node: {err}");
228 }
229 id
230}
231
232fn empty_subcompose_measure_result(constraints: Constraints) -> MeasureResult {
233 let (width, height) = constraints.constrain(0.0, 0.0);
234 MeasureResult::new(Size { width, height }, Vec::new())
235}
236
237#[composable(no_skip)]
240pub fn BoxWithConstraints<F>(modifier: Modifier, content: F) -> NodeId
241where
242 F: FnMut(BoxWithConstraintsScopeImpl) + 'static,
243{
244 let content_ref: Rc<RefCell<F>> = Rc::new(RefCell::new(content));
245 SubcomposeLayout(modifier, move |scope, constraints| {
246 let scope_impl = BoxWithConstraintsScopeImpl::new(constraints);
247 let scope_for_content = scope_impl;
248 let measurables = {
249 let content_ref = Rc::clone(&content_ref);
250 scope.subcompose(SlotId::new(0), constraints, move || {
251 cranpose_core::debug_label_current_scope("BoxWithConstraints.slot(0)");
252 let mut content = content_ref.borrow_mut();
253 content(scope_for_content);
254 })
255 };
256 let child_constraints = Constraints {
257 min_width: 0.0,
258 max_width: constraints.max_width,
259 min_height: 0.0,
260 max_height: constraints.max_height,
261 };
262
263 let mut width = 0.0_f32;
264 let mut height = 0.0_f32;
265 let mut placements = Vec::with_capacity(measurables.len());
266
267 for measurable in measurables {
268 let placeable = scope.measure(measurable, child_constraints);
269 width = width.max(placeable.width());
270 height = height.max(placeable.height());
271 placeable.place(0.0, 0.0);
272 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
273 }
274
275 width = width.clamp(constraints.min_width, constraints.max_width);
276 height = height.clamp(constraints.min_height, constraints.max_height);
277 scope.layout(width, height, placements)
278 })
279}
280
281#[cfg(test)]
282#[path = "tests/layout_tests.rs"]
283mod tests;