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);
105 config.is_clickable = true;
106 config.content_description = Some(description.clone());
107 });
108
109 let label_style = spec.label_style.resolve(spec.colors.on_primary);
110 let secondary_style = spec
111 .secondary_style
112 .resolve(fade(spec.colors.on_primary, spec.secondary_alpha));
113 Layout(
114 chrome,
115 WearButtonMeasurePolicy {
116 spec,
117 density: density.density(),
118 },
119 move || {
120 Text(label.clone(), Modifier::empty(), label_style.clone());
121 if let Some(secondary) = secondary_label.clone() {
122 Text(secondary, Modifier::empty(), secondary_style.clone());
123 }
124 },
125 )
126}
127
128fn fade(color: Color, alpha: f32) -> Color {
129 Color::rgba(color.0, color.1, color.2, color.3 * alpha)
130}
131
132#[derive(Clone, Debug, PartialEq)]
133struct WearButtonMeasurePolicy {
134 spec: WearButtonSpec,
135 density: f32,
136}
137
138impl MeasurePolicy for WearButtonMeasurePolicy {
139 fn measure(
140 &self,
141 scope: &dyn MeasureScope,
142 measurables: &[Box<dyn Measurable>],
143 constraints: Constraints,
144 ) -> MeasureResult {
145 let mut placements = Vec::new();
146 let size = self.measure_into(scope, measurables, constraints, &mut placements);
147 MeasureResult::new(size, placements)
148 }
149
150 fn measure_into(
151 &self,
152 _scope: &dyn MeasureScope,
153 measurables: &[Box<dyn Measurable>],
154 constraints: Constraints,
155 placements: &mut Vec<Placement>,
156 ) -> Size {
157 placements.clear();
158 let density = Density::new(self.density, 1.0);
159 let horizontal = density.dp(self.spec.padding_horizontal) * 2.0;
160 let vertical = density.dp(self.spec.padding_vertical) * 2.0;
161 let width = if constraints.max_width.is_finite() {
162 constraints.max_width
163 } else {
164 constraints.min_width
165 };
166 let available = (width - horizontal).max(0.0);
167 let child_constraints = Constraints {
168 min_width: 0.0,
169 max_width: available,
170 min_height: 0.0,
171 max_height: f32::INFINITY,
172 };
173
174 let mut placeables = Vec::with_capacity(measurables.len());
175 for measurable in measurables {
176 placeables.push(measurable.measure(child_constraints));
177 }
178
179 let spacing = density.dp(self.spec.label_spacing);
180 let column: f32 = placeables
181 .iter()
182 .map(|placeable| density.ceil(placeable.height()))
183 .sum::<f32>()
184 + spacing * placeables.len().saturating_sub(1) as f32;
185
186 let height = density
187 .dp(self.spec.min_height)
188 .max(density.ceil(column) + vertical)
189 .clamp(constraints.min_height, constraints.max_height);
190
191 let content = height - vertical;
192 let mut y = density.dp(self.spec.padding_vertical) + density.centre(content, column);
193 let x = density.dp(self.spec.padding_horizontal);
194 for placeable in &placeables {
195 placements.push(Placement::new(placeable.node_id(), x, y, 0));
196 y += density.ceil(placeable.height()) + spacing;
197 }
198 Size::new(width, height)
199 }
200
201 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
202 let density = Density::new(self.density, 1.0);
203 measurables
204 .iter()
205 .map(|m| m.min_intrinsic_width(height))
206 .fold(0.0, f32::max)
207 + density.dp(self.spec.padding_horizontal) * 2.0
208 }
209
210 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
211 let density = Density::new(self.density, 1.0);
212 measurables
213 .iter()
214 .map(|m| m.max_intrinsic_width(height))
215 .fold(0.0, f32::max)
216 + density.dp(self.spec.padding_horizontal) * 2.0
217 }
218
219 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
220 let density = Density::new(self.density, 1.0);
221 let spacing =
222 density.dp(self.spec.label_spacing) * measurables.len().saturating_sub(1) as f32;
223 let column = measurables
224 .iter()
225 .map(|m| m.min_intrinsic_height(width))
226 .sum::<f32>()
227 + spacing;
228 density
229 .dp(self.spec.min_height)
230 .max(density.ceil(column) + density.dp(self.spec.padding_vertical) * 2.0)
231 }
232
233 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
234 self.min_intrinsic_height(measurables, width)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn the_defaults_are_the_ones_the_tokens_declare() {
244 let spec = WearButtonSpec::default();
245 assert_eq!(spec.min_height, 52.0, "FilledButtonTokens.ContainerHeight");
246 assert_eq!(spec.corner_radius, 26.0, "ShapeTokens.CornerLarge");
247 assert_eq!(spec.padding_horizontal, 14.0);
248 assert_eq!(spec.padding_vertical, 6.0);
249 assert_eq!(spec.label_spacing, 1.0);
250 assert_eq!(spec.secondary_alpha, 0.8);
251 assert_eq!(spec.label_style, WearTextStyle::LABEL_MEDIUM);
252 assert_eq!(spec.secondary_style, WearTextStyle::LABEL_SMALL);
253 }
254
255 #[test]
256 fn a_secondary_label_draws_its_colour_at_four_fifths() {
257 let faded = fade(Color::rgba(1.0, 1.0, 1.0, 1.0), 0.8);
258 assert_eq!(faded.3, 0.8);
259 }
260}