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