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_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, &current_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}