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_layout::{MeasurePolicy, Placement};
15use std::cell::RefCell;
16use std::rc::Rc;
17
18#[composable]
19pub fn Layout<F, P>(modifier: Modifier, measure_policy: P, mut content: F) -> NodeId
20where
21 F: FnMut() + 'static,
22 P: MeasurePolicy + Clone + PartialEq + 'static,
23{
24 let policy: Rc<dyn MeasurePolicy> = Rc::new(measure_policy);
25 let modifier_for_reset = modifier.clone();
26 let policy_for_reset = Rc::clone(&policy);
27 let id = cranpose_core::with_current_composer(|composer| {
28 composer.emit_recyclable_node(
29 || LayoutNode::new(modifier.clone(), Rc::clone(&policy)),
30 move |node| {
31 *node = LayoutNode::new(modifier_for_reset.clone(), Rc::clone(&policy_for_reset));
32 },
33 )
34 });
35 if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut LayoutNode| {
36 node.set_modifier(modifier.clone());
37 node.set_measure_policy(Rc::clone(&policy));
38 }) {
39 debug_assert!(false, "failed to update Layout node: {err}");
40 }
41 cranpose_core::push_parent(id);
42 content();
43 cranpose_core::pop_parent();
44 id
45}
46
47#[composable]
48pub fn SubcomposeLayout(
49 modifier: Modifier,
50 measure_policy: impl for<'scope> Fn(&mut SubcomposeMeasureScopeImpl<'scope>, Constraints) -> MeasureResult
51 + 'static,
52) -> NodeId {
53 cranpose_core::debug_label_current_scope("SubcomposeLayout");
54 let policy_cell =
55 cranpose_core::remember(|| Rc::new(RefCell::new(None::<Rc<SubcomposeMeasurePolicy>>)))
56 .with(|cell| cell.clone());
57 let current_policy: Rc<SubcomposeMeasurePolicy> = Rc::new(measure_policy);
58 let policy_captures_changed = {
59 let mut policy_cell_ref = policy_cell.borrow_mut();
60 let changed = policy_cell_ref
61 .as_ref()
62 .is_none_or(|previous| !Rc::ptr_eq(previous, ¤t_policy));
63 *policy_cell_ref = Some(current_policy);
64 changed
65 };
66 let policy: Rc<SubcomposeMeasurePolicy> = cranpose_core::remember(move || {
67 let policy_cell = policy_cell.clone();
68 let policy: Rc<SubcomposeMeasurePolicy> = Rc::new(move |scope, constraints| {
69 let current = {
70 policy_cell
71 .borrow()
72 .as_ref()
73 .expect("subcompose measure policy should be initialized")
74 .clone()
75 };
76 current(scope, constraints)
77 });
78 policy
79 })
80 .with(|policy| policy.clone());
81 let id = cranpose_core::with_current_composer(|composer| {
82 composer.emit_node(|| SubcomposeLayoutNode::new(modifier.clone(), Rc::clone(&policy)))
83 });
84 if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut SubcomposeLayoutNode| {
85 node.set_modifier(modifier.clone());
86 node.set_measure_policy(Rc::clone(&policy));
87 if policy_captures_changed {
88 node.request_measure_recompose();
89 }
90 }) {
91 debug_assert!(false, "failed to update SubcomposeLayout node: {err}");
92 }
93 id
94}
95
96#[composable(no_skip)]
97pub fn BoxWithConstraints<F>(modifier: Modifier, content: F) -> NodeId
98where
99 F: FnMut(BoxWithConstraintsScopeImpl) + 'static,
100{
101 let content_ref: Rc<RefCell<F>> = Rc::new(RefCell::new(content));
102 SubcomposeLayout(modifier, move |scope, constraints| {
103 let scope_impl = BoxWithConstraintsScopeImpl::new(constraints);
104 let scope_for_content = scope_impl;
105 let measurables = {
106 let content_ref = Rc::clone(&content_ref);
107 scope.subcompose(SlotId::new(0), move || {
108 cranpose_core::debug_label_current_scope("BoxWithConstraints.slot(0)");
109 let mut content = content_ref.borrow_mut();
110 content(scope_for_content);
111 })
112 };
113 let child_constraints = Constraints {
114 min_width: 0.0,
115 max_width: constraints.max_width,
116 min_height: 0.0,
117 max_height: constraints.max_height,
118 };
119
120 let mut width = 0.0_f32;
121 let mut height = 0.0_f32;
122 let mut placements = Vec::with_capacity(measurables.len());
123
124 for measurable in measurables {
125 let placeable = scope.measure(measurable, child_constraints);
126 width = width.max(placeable.width());
127 height = height.max(placeable.height());
128 placeable.place(0.0, 0.0);
129 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
130 }
131
132 width = width.clamp(constraints.min_width, constraints.max_width);
133 height = height.clamp(constraints.min_height, constraints.max_height);
134 scope.layout(width, height, placements)
135 })
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use cranpose_core::{location_key, Composition, MemoryApplier, MutableState};
142 use std::cell::Cell;
143
144 #[test]
145 fn layout_recomposes_when_content_reads_state() {
146 thread_local! {
147 static INVOCATIONS: Cell<usize> = const { Cell::new(0) };
148 }
149
150 let mut composition = Composition::new(MemoryApplier::new());
151 let runtime = composition.runtime_handle();
152 let state = MutableState::with_runtime(0_i32, runtime);
153
154 composition
155 .render(location_key(file!(), line!(), column!()), {
156 let observed_state = state;
157 move || {
158 Layout(
159 Modifier::empty(),
160 crate::layout::policies::EmptyMeasurePolicy,
161 {
162 let observed_state = observed_state;
163 move || {
164 let _ = observed_state.value();
165 INVOCATIONS.with(|calls| calls.set(calls.get() + 1));
166 }
167 },
168 );
169 }
170 })
171 .expect("initial layout render");
172
173 INVOCATIONS.with(|calls| assert_eq!(calls.get(), 1));
174
175 state.set_value(1);
176 composition
177 .process_invalid_scopes()
178 .expect("layout content recomposition");
179
180 INVOCATIONS.with(|calls| assert_eq!(calls.get(), 2));
181 }
182}