cranpose_ui/widgets/wear/
button.rs1#![allow(non_snake_case)]
21
22use cranpose_core::NodeId;
23use cranpose_ui_graphics::{DrawScope, Size};
24use cranpose_ui_layout::{
25 Constraints, Measurable, MeasurePolicy, MeasureResult, MeasureScope, Placement,
26};
27
28use crate::{
29 SemanticsWidgetRole, composable,
30 density::Density,
31 modifier::{Brush, Color, CornerRadii, Modifier, SemanticsConfiguration},
32 widgets::{
33 Layout, Text,
34 wear::theme::{WearColors, WearTextStyle},
35 },
36};
37
38#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct WearButtonSpec {
41 pub colors: WearColors,
42 pub label_style: WearTextStyle,
43 pub secondary_style: WearTextStyle,
44 pub min_height: f32,
46 pub corner_radius: f32,
48 pub padding_horizontal: f32,
49 pub padding_vertical: f32,
50 pub label_spacing: f32,
52 pub secondary_alpha: f32,
54}
55
56impl Default for WearButtonSpec {
57 fn default() -> Self {
58 Self {
59 colors: WearColors::default(),
60 label_style: WearTextStyle::LABEL_MEDIUM,
61 secondary_style: WearTextStyle::LABEL_SMALL,
62 min_height: 52.0,
63 corner_radius: 26.0,
64 padding_horizontal: 14.0,
65 padding_vertical: 6.0,
66 label_spacing: 1.0,
67 secondary_alpha: 0.8,
68 }
69 }
70}
71
72impl WearButtonSpec {
73 pub fn colors(mut self, colors: WearColors) -> Self {
74 self.colors = colors;
75 self
76 }
77}
78
79#[composable]
81pub fn WearButton<F>(
82 modifier: Modifier,
83 spec: WearButtonSpec,
84 label: String,
85 secondary_label: Option<String>,
86 on_click: F,
87) -> NodeId
88where
89 F: FnMut() + 'static,
90{
91 let density = crate::density::density();
92 let container = spec.colors.primary;
93 let radius = density.dp(spec.corner_radius);
94 let description = match &secondary_label {
95 Some(secondary) => format!("{label}, {secondary}"),
96 None => label.clone(),
97 };
98 let chrome = modifier
99 .draw_behind(move |scope: &mut dyn DrawScope| {
100 scope.draw_round_rect(Brush::Solid(container), CornerRadii::uniform(radius));
101 })
102 .clickable(move |_point| on_click())
103 .semantics(move |config: &mut SemanticsConfiguration| {
104 config.role = Some(SemanticsWidgetRole::Button);
106 config.is_clickable = true;
107 config.content_description = Some(description.clone());
108 });
109
110 let label_style = spec.label_style.resolve(spec.colors.on_primary);
111 let secondary_style = spec
112 .secondary_style
113 .resolve(fade(spec.colors.on_primary, spec.secondary_alpha));
114 Layout(
115 chrome,
116 WearButtonMeasurePolicy {
117 spec,
118 density: density.density(),
119 },
120 move || {
121 Text(label.clone(), Modifier::empty(), label_style.clone());
122 if let Some(secondary) = secondary_label.clone() {
123 Text(secondary, Modifier::empty(), secondary_style.clone());
124 }
125 },
126 )
127}
128
129fn fade(color: Color, alpha: f32) -> Color {
130 Color::rgba(color.0, color.1, color.2, color.3 * alpha)
131}
132
133#[derive(Clone, Debug, PartialEq)]
134struct WearButtonMeasurePolicy {
135 spec: WearButtonSpec,
136 density: f32,
137}
138
139impl MeasurePolicy for WearButtonMeasurePolicy {
140 fn measure(
141 &self,
142 scope: &dyn MeasureScope,
143 measurables: &[Box<dyn Measurable>],
144 constraints: Constraints,
145 ) -> MeasureResult {
146 let mut placements = Vec::new();
147 let size = self.measure_into(scope, measurables, constraints, &mut placements);
148 MeasureResult::new(size, placements)
149 }
150
151 fn measure_into(
152 &self,
153 _scope: &dyn MeasureScope,
154 measurables: &[Box<dyn Measurable>],
155 constraints: Constraints,
156 placements: &mut Vec<Placement>,
157 ) -> Size {
158 placements.clear();
159 let density = Density::new(self.density, 1.0);
160 let horizontal = density.dp(self.spec.padding_horizontal) * 2.0;
161 let vertical = density.dp(self.spec.padding_vertical) * 2.0;
162 let width = if constraints.max_width.is_finite() {
163 constraints.max_width
164 } else {
165 constraints.min_width
166 };
167 let available = (width - horizontal).max(0.0);
168 let child_constraints = Constraints {
169 min_width: 0.0,
170 max_width: available,
171 min_height: 0.0,
172 max_height: f32::INFINITY,
173 };
174
175 let mut placeables = Vec::with_capacity(measurables.len());
176 for measurable in measurables {
177 placeables.push(measurable.measure(child_constraints));
178 }
179
180 let spacing = density.dp(self.spec.label_spacing);
181 let column: f32 = placeables
182 .iter()
183 .map(|placeable| density.ceil(placeable.height()))
184 .sum::<f32>()
185 + spacing * placeables.len().saturating_sub(1) as f32;
186
187 let height = density
188 .dp(self.spec.min_height)
189 .max(density.ceil(column) + vertical)
190 .clamp(constraints.min_height, constraints.max_height);
191
192 let content = height - vertical;
196 let mut y = density.dp(self.spec.padding_vertical) + density.centre(content, column);
197 let x = density.dp(self.spec.padding_horizontal);
198 for placeable in &placeables {
199 placements.push(Placement::new(placeable.node_id(), x, y, 0));
202 y += density.ceil(placeable.height()) + spacing;
203 }
204 Size::new(width, height)
205 }
206
207 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
208 let density = Density::new(self.density, 1.0);
209 measurables
210 .iter()
211 .map(|m| m.min_intrinsic_width(height))
212 .fold(0.0, f32::max)
213 + density.dp(self.spec.padding_horizontal) * 2.0
214 }
215
216 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
217 let density = Density::new(self.density, 1.0);
218 measurables
219 .iter()
220 .map(|m| m.max_intrinsic_width(height))
221 .fold(0.0, f32::max)
222 + density.dp(self.spec.padding_horizontal) * 2.0
223 }
224
225 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
226 let density = Density::new(self.density, 1.0);
227 let spacing =
228 density.dp(self.spec.label_spacing) * measurables.len().saturating_sub(1) as f32;
229 let column = measurables
230 .iter()
231 .map(|m| m.min_intrinsic_height(width))
232 .sum::<f32>()
233 + spacing;
234 density
235 .dp(self.spec.min_height)
236 .max(density.ceil(column) + density.dp(self.spec.padding_vertical) * 2.0)
237 }
238
239 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
240 self.min_intrinsic_height(measurables, width)
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn the_defaults_are_the_ones_the_tokens_declare() {
250 let spec = WearButtonSpec::default();
251 assert_eq!(spec.min_height, 52.0, "FilledButtonTokens.ContainerHeight");
252 assert_eq!(spec.corner_radius, 26.0, "ShapeTokens.CornerLarge");
253 assert_eq!(spec.padding_horizontal, 14.0);
254 assert_eq!(spec.padding_vertical, 6.0);
255 assert_eq!(spec.label_spacing, 1.0);
256 assert_eq!(spec.secondary_alpha, 0.8);
257 assert_eq!(spec.label_style, WearTextStyle::LABEL_MEDIUM);
258 assert_eq!(spec.secondary_style, WearTextStyle::LABEL_SMALL);
259 }
260
261 #[test]
262 fn a_secondary_label_draws_its_colour_at_four_fifths() {
263 let faded = fade(Color::rgba(1.0, 1.0, 1.0, 1.0), 0.8);
264 assert_eq!(faded.3, 0.8);
265 }
266}