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