Skip to main content

cranpose_ui/widgets/
layout.rs

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