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::LayoutModifierMeasureResult;
9
10use super::Modifier;
11
12/// The side of the smallest target a person presses with ease: Material's
13/// 48 dp, above Apple's 44 pt.
14pub const MINIMUM_INTERACTIVE_SIZE: f32 = 48.0;
15
16/// The size a control takes and where its content sits inside it: at least
17/// the minimum on each side, as far as the incoming constraints allow, with
18/// the content in the middle.
19pub fn minimum_interactive_placement(
20    constraints: Constraints,
21    content: Size,
22) -> LayoutModifierMeasureResult {
23    let width = content.width.max(
24        MINIMUM_INTERACTIVE_SIZE
25            .max(constraints.min_width)
26            .min(constraints.max_width),
27    );
28    let height = content.height.max(
29        MINIMUM_INTERACTIVE_SIZE
30            .max(constraints.min_height)
31            .min(constraints.max_height),
32    );
33    LayoutModifierMeasureResult::new(
34        Size { width, height },
35        ((width - content.width) / 2.0).round(),
36        ((height - content.height) / 2.0).round(),
37    )
38}
39
40/// The element behind [`Modifier::minimum_interactive_component_size`].
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
42pub struct MinimumInteractiveElement;
43
44/// The layout node that widens a control to the minimum target size.
45pub struct MinimumInteractiveNode {
46    state: NodeState,
47}
48
49impl DelegatableNode for MinimumInteractiveNode {
50    fn node_state(&self) -> &NodeState {
51        &self.state
52    }
53}
54
55crate::modifier_nodes::impl_layout_modifier_node!(
56    MinimumInteractiveNode,
57    invalidate = InvalidationKind::Layout
58);
59
60impl LayoutModifierNode for MinimumInteractiveNode {
61    fn measure(
62        &self,
63        _context: &mut dyn ModifierNodeContext,
64        measurable: &dyn Measurable,
65        constraints: Constraints,
66    ) -> LayoutModifierMeasureResult {
67        let placeable = measurable.measure(constraints);
68        minimum_interactive_placement(
69            constraints,
70            Size {
71                width: placeable.width(),
72                height: placeable.height(),
73            },
74        )
75    }
76
77    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
78        measurable
79            .min_intrinsic_width(height)
80            .max(MINIMUM_INTERACTIVE_SIZE)
81    }
82
83    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
84        measurable
85            .max_intrinsic_width(height)
86            .max(MINIMUM_INTERACTIVE_SIZE)
87    }
88
89    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
90        measurable
91            .min_intrinsic_height(width)
92            .max(MINIMUM_INTERACTIVE_SIZE)
93    }
94
95    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
96        measurable
97            .max_intrinsic_height(width)
98            .max(MINIMUM_INTERACTIVE_SIZE)
99    }
100}
101
102impl ModifierNodeElement for MinimumInteractiveElement {
103    type Node = MinimumInteractiveNode;
104
105    fn create(&self) -> Self::Node {
106        MinimumInteractiveNode {
107            state: NodeState::new(),
108        }
109    }
110
111    fn update(&self, _node: &mut Self::Node) {}
112
113    fn capabilities(&self) -> NodeCapabilities {
114        NodeCapabilities::LAYOUT
115    }
116}
117
118impl Modifier {
119    /// Keeps at least 48 by 48 points for a press on this control, as far as
120    /// the parent allows, and puts the drawn content in the middle. A small
121    /// icon or a checkbox stays easy to hit with a finger, a switch scan or a
122    /// shaky hand; the drawn size does not change. Compose's
123    /// `Modifier.minimumInteractiveComponentSize()`.
124    pub fn minimum_interactive_component_size(self) -> Self {
125        self.then(Self::with_element(MinimumInteractiveElement))
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn loose() -> Constraints {
134        Constraints {
135            min_width: 0.0,
136            max_width: 400.0,
137            min_height: 0.0,
138            max_height: 400.0,
139        }
140    }
141
142    #[test]
143    fn a_small_control_grows_to_the_minimum_and_centres_its_content() {
144        let result = minimum_interactive_placement(
145            loose(),
146            Size {
147                width: 24.0,
148                height: 20.0,
149            },
150        );
151        assert_eq!((result.size.width, result.size.height), (48.0, 48.0));
152        assert_eq!(
153            (result.placement_offset_x, result.placement_offset_y),
154            (12.0, 14.0)
155        );
156    }
157
158    #[test]
159    fn a_large_control_keeps_its_own_size() {
160        let result = minimum_interactive_placement(
161            loose(),
162            Size {
163                width: 120.0,
164                height: 56.0,
165            },
166        );
167        assert_eq!((result.size.width, result.size.height), (120.0, 56.0));
168        assert_eq!(
169            (result.placement_offset_x, result.placement_offset_y),
170            (0.0, 0.0)
171        );
172    }
173
174    #[test]
175    fn a_tight_parent_wins_over_the_minimum() {
176        let tight = Constraints {
177            min_width: 0.0,
178            max_width: 30.0,
179            min_height: 0.0,
180            max_height: 100.0,
181        };
182        let result = minimum_interactive_placement(
183            tight,
184            Size {
185                width: 24.0,
186                height: 24.0,
187            },
188        );
189        assert_eq!((result.size.width, result.size.height), (30.0, 48.0));
190        assert_eq!(
191            (result.placement_offset_x, result.placement_offset_y),
192            (3.0, 12.0)
193        );
194    }
195}