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