Skip to main content

cranpose_ui/modifier/
minimum_interactive.rs

1//! The target a finger or a switch scan reaches with ease: a control keeps at
2//! least 48 by 48 points for a press, whatever it draws.
3
4use cranpose_foundation::{
5    Constraints, DelegatableNode, InvalidationKind, LayoutModifierNode, Measurable, ModifierNode,
6    ModifierNodeContext, ModifierNodeElement, NodeCapabilities, NodeState, Size,
7};
8use cranpose_ui_layout::{
9    HorizontalAlignment, LayoutModifierMeasureResult, VerticalAlignment, round_to_px,
10};
11
12use super::Modifier;
13
14/// The side of the smallest target a person presses with ease: Material's
15/// 48 dp, above Apple's 44 pt.
16pub const MINIMUM_INTERACTIVE_SIZE: f32 = 48.0;
17
18/// The size a control takes and where its content sits inside it: at least
19/// the minimum on each side, as far as the incoming constraints allow, with
20/// the content in the middle on the device pixel grid of `density`.
21pub fn minimum_interactive_placement(
22    constraints: Constraints,
23    content: Size,
24    density: f32,
25) -> LayoutModifierMeasureResult {
26    let minimum = round_to_px(MINIMUM_INTERACTIVE_SIZE, density);
27    let width = content.width.max(
28        minimum
29            .max(constraints.min_width)
30            .min(constraints.max_width),
31    );
32    let height = content.height.max(
33        minimum
34            .max(constraints.min_height)
35            .min(constraints.max_height),
36    );
37    LayoutModifierMeasureResult::new(
38        Size { width, height },
39        HorizontalAlignment::CenterHorizontally.align(width, content.width, density),
40        VerticalAlignment::CenterVertically.align(height, content.height, density),
41    )
42}
43
44/// The element behind [`Modifier::minimum_interactive_component_size`].
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
46pub struct MinimumInteractiveElement;
47
48/// The layout node that widens a control to the minimum target size.
49pub struct MinimumInteractiveNode {
50    state: NodeState,
51}
52
53impl DelegatableNode for MinimumInteractiveNode {
54    fn node_state(&self) -> &NodeState {
55        &self.state
56    }
57}
58
59crate::modifier_nodes::impl_layout_modifier_node!(
60    MinimumInteractiveNode,
61    invalidate = InvalidationKind::Layout
62);
63
64impl LayoutModifierNode for MinimumInteractiveNode {
65    fn measure(
66        &self,
67        context: &mut dyn ModifierNodeContext,
68        measurable: &dyn Measurable,
69        constraints: Constraints,
70    ) -> LayoutModifierMeasureResult {
71        let placeable = measurable.measure(constraints);
72        minimum_interactive_placement(
73            constraints,
74            Size {
75                width: placeable.width(),
76                height: placeable.height(),
77            },
78            context.density(),
79        )
80    }
81
82    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32, density: f32) -> f32 {
83        measurable
84            .min_intrinsic_width(height)
85            .max(round_to_px(MINIMUM_INTERACTIVE_SIZE, density))
86    }
87
88    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32, density: f32) -> f32 {
89        measurable
90            .max_intrinsic_width(height)
91            .max(round_to_px(MINIMUM_INTERACTIVE_SIZE, density))
92    }
93
94    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32, density: f32) -> f32 {
95        measurable
96            .min_intrinsic_height(width)
97            .max(round_to_px(MINIMUM_INTERACTIVE_SIZE, density))
98    }
99
100    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32, density: f32) -> f32 {
101        measurable
102            .max_intrinsic_height(width)
103            .max(round_to_px(MINIMUM_INTERACTIVE_SIZE, density))
104    }
105}
106
107impl ModifierNodeElement for MinimumInteractiveElement {
108    type Node = MinimumInteractiveNode;
109
110    fn create(&self) -> Self::Node {
111        MinimumInteractiveNode {
112            state: NodeState::new(),
113        }
114    }
115
116    fn update(&self, _node: &mut Self::Node) {}
117
118    fn capabilities(&self) -> NodeCapabilities {
119        NodeCapabilities::LAYOUT
120    }
121}
122
123impl Modifier {
124    /// Keeps at least 48 by 48 points for a press on this control, as far as
125    /// the parent allows, and puts the drawn content in the middle. A small
126    /// icon or a checkbox stays easy to hit with a finger, a switch scan or a
127    /// shaky hand; the drawn size does not change. Compose's
128    /// `Modifier.minimumInteractiveComponentSize()`.
129    pub fn minimum_interactive_component_size(self) -> Self {
130        self.then(Self::with_element(MinimumInteractiveElement))
131    }
132}
133
134#[cfg(test)]
135#[path = "tests/minimum_interactive_tests.rs"]
136mod tests;