cranpose_ui/modifier/
minimum_interactive.rs1use 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
12pub const MINIMUM_INTERACTIVE_SIZE: f32 = 48.0;
15
16pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
42pub struct MinimumInteractiveElement;
43
44pub 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 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}