cranpose_ui/widgets/wear/
button.rs1use cranpose_core::NodeId;
21use cranpose_ui_graphics::{DrawScope, Size};
22use cranpose_ui_layout::{
23 Constraints, Measurable, MeasurePolicy, MeasureResult, MeasureScope, Placement,
24};
25
26use crate::{
27 SemanticsWidgetRole, composable,
28 density::Density,
29 modifier::{Brush, Color, CornerRadii, Modifier, SemanticsConfiguration},
30 widgets::{
31 Layout, Text,
32 wear::theme::{WearColors, WearTextStyle},
33 },
34};
35
36#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct WearButtonSpec {
39 pub colors: WearColors,
40 pub label_style: WearTextStyle,
41 pub secondary_style: WearTextStyle,
42 pub min_height: f32,
44 pub corner_radius: f32,
46 pub padding_horizontal: f32,
47 pub padding_vertical: f32,
48 pub label_spacing: f32,
50 pub secondary_alpha: f32,
52}
53
54impl Default for WearButtonSpec {
55 fn default() -> Self {
56 Self {
57 colors: WearColors::default(),
58 label_style: WearTextStyle::LABEL_MEDIUM,
59 secondary_style: WearTextStyle::LABEL_SMALL,
60 min_height: 52.0,
61 corner_radius: 26.0,
62 padding_horizontal: 14.0,
63 padding_vertical: 6.0,
64 label_spacing: 1.0,
65 secondary_alpha: 0.8,
66 }
67 }
68}
69
70impl WearButtonSpec {
71 pub fn colors(mut self, colors: WearColors) -> Self {
72 self.colors = colors;
73 self
74 }
75}
76
77#[composable]
79pub fn WearButton<F>(
80 modifier: Modifier,
81 spec: WearButtonSpec,
82 label: String,
83 secondary_label: Option<String>,
84 on_click: F,
85) -> NodeId
86where
87 F: FnMut() + 'static,
88{
89 let density = crate::density::density();
90 let container = spec.colors.primary;
91 let radius = density.dp(spec.corner_radius);
92 let description = match &secondary_label {
93 Some(secondary) => format!("{label}, {secondary}"),
94 None => label.clone(),
95 };
96 let chrome = modifier
97 .draw_behind(move |scope: &mut dyn DrawScope| {
98 scope.draw_round_rect(Brush::Solid(container), CornerRadii::uniform(radius));
99 })
100 .clickable(move |_point| on_click())
101 .stable_semantics(move |config: &mut SemanticsConfiguration| {
102 config.role = Some(SemanticsWidgetRole::Button);
103 config.is_clickable = true;
104 config.content_description = Some(description.clone());
105 });
106
107 let label_style = spec.label_style.resolve(spec.colors.on_primary);
108 let secondary_style = spec
109 .secondary_style
110 .resolve(fade(spec.colors.on_primary, spec.secondary_alpha));
111 Layout(
112 chrome,
113 WearButtonMeasurePolicy {
114 spec,
115 density: density.density(),
116 },
117 move || {
118 Text(label.clone(), Modifier::empty(), label_style.clone());
119 if let Some(secondary) = secondary_label.clone() {
120 Text(secondary, Modifier::empty(), secondary_style.clone());
121 }
122 },
123 )
124}
125
126fn fade(color: Color, alpha: f32) -> Color {
127 Color::rgba(color.0, color.1, color.2, color.3 * alpha)
128}
129
130#[derive(Clone, Debug, PartialEq)]
131struct WearButtonMeasurePolicy {
132 spec: WearButtonSpec,
133 density: f32,
134}
135
136impl MeasurePolicy for WearButtonMeasurePolicy {
137 fn measure(
138 &self,
139 scope: &dyn MeasureScope,
140 measurables: &[Box<dyn Measurable>],
141 constraints: Constraints,
142 ) -> MeasureResult {
143 let mut placements = Vec::new();
144 let measurement = self.measure_into(scope, measurables, constraints, &mut placements);
145 MeasureResult::new(measurement, placements)
146 }
147
148 fn measure_into(
149 &self,
150 _scope: &dyn MeasureScope,
151 measurables: &[Box<dyn Measurable>],
152 constraints: Constraints,
153 placements: &mut Vec<Placement>,
154 ) -> cranpose_ui_layout::Measurement {
155 placements.clear();
156 let density = Density::new(self.density, 1.0);
157 let horizontal = density.dp(self.spec.padding_horizontal) * 2.0;
158 let vertical = density.dp(self.spec.padding_vertical) * 2.0;
159 let width = if constraints.max_width.is_finite() {
160 constraints.max_width
161 } else {
162 constraints.min_width
163 };
164 let available = (width - horizontal).max(0.0);
165 let child_constraints = Constraints {
166 min_width: 0.0,
167 max_width: available,
168 min_height: 0.0,
169 max_height: f32::INFINITY,
170 };
171
172 let mut placeables = Vec::with_capacity(measurables.len());
173 for measurable in measurables {
174 placeables.push(measurable.measure(child_constraints));
175 }
176
177 let spacing = density.dp(self.spec.label_spacing);
178 let column: f32 = placeables
179 .iter()
180 .map(|placeable| density.ceil(placeable.height()))
181 .sum::<f32>()
182 + spacing * placeables.len().saturating_sub(1) as f32;
183
184 let height = density
185 .dp(self.spec.min_height)
186 .max(density.ceil(column) + vertical)
187 .clamp(constraints.min_height, constraints.max_height);
188
189 let content = height - vertical;
190 let mut y = density.dp(self.spec.padding_vertical) + density.centre(content, column);
191 let x = density.dp(self.spec.padding_horizontal);
192 for placeable in &placeables {
193 placements.push(Placement::new(placeable.node_id(), x, y, 0));
194 y += density.ceil(placeable.height()) + spacing;
195 }
196 Size::new(width, height).into()
197 }
198
199 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
200 let density = Density::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 = Density::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 = Density::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)]
237#[path = "tests/button_tests.rs"]
238mod tests;