Skip to main content

cranpose_ui/widgets/
layout.rs

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