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