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