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, 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>(modifier: Modifier, measure_policy: P, mut content: F) -> NodeId
46where
47    F: FnMut() + 'static,
48    P: MeasurePolicy + Clone + PartialEq + 'static,
49{
50    let policy_holder = cranpose_core::remember({
51        let measure_policy = measure_policy.clone();
52        move || {
53            Rc::new(RefCell::new(RetainedMeasurePolicy {
54                value: measure_policy.clone(),
55                policy: Rc::new(measure_policy),
56            }))
57        }
58    })
59    .with(Rc::clone);
60    let policy = {
61        let mut holder = policy_holder.borrow_mut();
62        if holder.value != measure_policy {
63            holder.value = measure_policy.clone();
64            holder.policy = Rc::new(measure_policy);
65        }
66        Rc::clone(&holder.policy)
67    };
68    let modifier_for_reset = modifier.clone();
69    let policy_for_reset = Rc::clone(&policy);
70    let id = cranpose_core::with_current_composer(|composer| {
71        composer.emit_recyclable_node(
72            || LayoutNode::new(modifier.clone(), Rc::clone(&policy)),
73            move |node| {
74                *node = LayoutNode::new(modifier_for_reset.clone(), Rc::clone(&policy_for_reset));
75            },
76        )
77    });
78    let composed_density = crate::density::density();
79    if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut LayoutNode| {
80        node.set_modifier(modifier.clone());
81        node.set_measure_policy(Rc::clone(&policy));
82        node.set_density(composed_density);
83    }) {
84        debug_assert!(false, "failed to update Layout node: {err}");
85    }
86    cranpose_core::push_parent(id);
87    compose_under_modifier_locals(&modifier, &mut content);
88    cranpose_core::pop_parent();
89    id
90}
91
92fn compose_under_modifier_locals(modifier: &Modifier, content: &mut dyn FnMut()) {
93    let provided = modifier.provided_composition_locals();
94    if provided.is_empty() {
95        content();
96        return;
97    }
98    cranpose_core::CompositionLocalProvider(provided, content);
99}
100
101#[composable]
102pub fn SubcomposeLayout(
103    modifier: Modifier,
104    measure_policy: impl for<'scope> Fn(
105        &mut SubcomposeMeasureScopeImpl<'scope>,
106        Constraints,
107    ) -> MeasureResult
108    + 'static,
109) -> NodeId {
110    cranpose_core::debug_label_current_scope("SubcomposeLayout");
111    let policy_cell =
112        cranpose_core::remember(|| Rc::new(RefCell::new(None::<Rc<SubcomposeMeasurePolicy>>)))
113            .with(|cell| cell.clone());
114    let current_policy: Rc<SubcomposeMeasurePolicy> = Rc::new(measure_policy);
115    let policy_captures_changed = {
116        let mut policy_cell_ref = policy_cell.borrow_mut();
117        let changed = policy_cell_ref
118            .as_ref()
119            .is_none_or(|previous| !Rc::ptr_eq(previous, &current_policy));
120        *policy_cell_ref = Some(current_policy);
121        changed
122    };
123    let policy: Rc<SubcomposeMeasurePolicy> = cranpose_core::remember(move || {
124        let policy_cell = policy_cell.clone();
125        let policy: Rc<SubcomposeMeasurePolicy> =
126            Rc::new(
127                move |scope, constraints| match policy_cell.borrow().as_ref().cloned() {
128                    Some(current) => current(scope, constraints),
129                    None => empty_subcompose_measure_result(constraints),
130                },
131            );
132        policy
133    })
134    .with(|policy| policy.clone());
135    let id = cranpose_core::with_current_composer(|composer| {
136        composer.emit_node(|| SubcomposeLayoutNode::new(modifier.clone(), Rc::clone(&policy)))
137    });
138    let captured_context =
139        cranpose_core::with_current_composer(|composer| composer.capture_composition_context());
140    let composed_density = crate::density::density();
141    if let Err(err) = cranpose_core::with_node_mut(id, |node: &mut SubcomposeLayoutNode| {
142        node.set_modifier(modifier.clone());
143        node.set_measure_policy(Rc::clone(&policy));
144        node.set_captured_context(captured_context.clone());
145        node.set_density(composed_density);
146        if policy_captures_changed {
147            node.invalidate_subcomposition();
148        }
149    }) {
150        debug_assert!(false, "failed to update SubcomposeLayout node: {err}");
151    }
152    id
153}
154
155fn empty_subcompose_measure_result(constraints: Constraints) -> MeasureResult {
156    let (width, height) = constraints.constrain(0.0, 0.0);
157    MeasureResult::new(Size { width, height }, Vec::new())
158}
159
160#[composable(no_skip)]
161pub fn BoxWithConstraints<F>(modifier: Modifier, content: F) -> NodeId
162where
163    F: FnMut(BoxWithConstraintsScopeImpl) + 'static,
164{
165    let content_ref: Rc<RefCell<F>> = Rc::new(RefCell::new(content));
166    SubcomposeLayout(modifier, move |scope, constraints| {
167        let scope_impl = BoxWithConstraintsScopeImpl::new(constraints);
168        let scope_for_content = scope_impl;
169        let measurables = {
170            let content_ref = Rc::clone(&content_ref);
171            scope.subcompose(SlotId::new(0), constraints, move || {
172                cranpose_core::debug_label_current_scope("BoxWithConstraints.slot(0)");
173                let mut content = content_ref.borrow_mut();
174                content(scope_for_content);
175            })
176        };
177        let child_constraints = Constraints {
178            min_width: 0.0,
179            max_width: constraints.max_width,
180            min_height: 0.0,
181            max_height: constraints.max_height,
182        };
183
184        let mut width = 0.0_f32;
185        let mut height = 0.0_f32;
186        let mut placements = Vec::with_capacity(measurables.len());
187
188        for measurable in measurables {
189            let placeable = scope.measure(measurable, child_constraints);
190            width = width.max(placeable.width());
191            height = height.max(placeable.height());
192            placeable.place(0.0, 0.0);
193            placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
194        }
195
196        width = width.clamp(constraints.min_width, constraints.max_width);
197        height = height.clamp(constraints.min_height, constraints.max_height);
198        scope.layout(width, height, placements)
199    })
200}
201
202#[cfg(test)]
203#[path = "tests/layout_tests.rs"]
204mod tests;