cranpose_ui/widgets/wear/
list_header.rs1#![allow(non_snake_case)]
25
26use crate::composable;
27use crate::modifier::Modifier;
28use crate::text::paragraph::TextAlign;
29use crate::widgets::wear::density::WearDensity;
30use crate::widgets::wear::theme::{WearColors, WearTextStyle};
31use crate::widgets::{Layout, Text};
32use cranpose_core::NodeId;
33use cranpose_ui_graphics::Size;
34use cranpose_ui_layout::{Constraints, Measurable, MeasurePolicy, MeasureResult, Placement};
35
36#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct ListHeaderSpec {
39 pub colors: WearColors,
40 pub text_style: WearTextStyle,
41 pub min_height: f32,
43 pub padding_start: f32,
44 pub padding_top: f32,
45 pub padding_end: f32,
46 pub padding_bottom: f32,
47}
48
49impl Default for ListHeaderSpec {
50 fn default() -> Self {
51 Self {
52 colors: WearColors::default(),
53 text_style: WearTextStyle::TITLE_MEDIUM.aligned(TextAlign::Center),
59 min_height: 48.0,
60 padding_start: 14.0,
61 padding_top: 16.0,
62 padding_end: 14.0,
63 padding_bottom: 12.0,
64 }
65 }
66}
67
68impl ListHeaderSpec {
69 pub fn colors(mut self, colors: WearColors) -> Self {
70 self.colors = colors;
71 self
72 }
73}
74
75#[composable]
83pub fn ListHeader<S>(modifier: Modifier, spec: ListHeaderSpec, label: S) -> NodeId
84where
85 S: crate::widgets::IntoTextSource + Clone + PartialEq + 'static,
86{
87 let style = spec.text_style.resolve(spec.colors.on_background);
88 Layout(
89 modifier,
90 ListHeaderMeasurePolicy {
91 spec,
92 density: WearDensity::current().density(),
93 },
94 move || {
95 Text(label.clone(), Modifier::empty(), style.clone());
96 },
97 )
98}
99
100#[derive(Clone, Debug, PartialEq)]
101struct ListHeaderMeasurePolicy {
102 spec: ListHeaderSpec,
103 density: f32,
104}
105
106impl ListHeaderMeasurePolicy {
107 fn horizontal(&self, density: WearDensity) -> f32 {
108 density.dp(self.spec.padding_start) + density.dp(self.spec.padding_end)
109 }
110
111 fn vertical(&self, density: WearDensity) -> f32 {
112 density.dp(self.spec.padding_top) + density.dp(self.spec.padding_bottom)
113 }
114}
115
116impl MeasurePolicy for ListHeaderMeasurePolicy {
117 fn measure(
118 &self,
119 measurables: &[Box<dyn Measurable>],
120 constraints: Constraints,
121 ) -> MeasureResult {
122 let mut placements = Vec::new();
123 let size = self.measure_into(measurables, constraints, &mut placements);
124 MeasureResult::new(size, placements)
125 }
126
127 fn measure_into(
128 &self,
129 measurables: &[Box<dyn Measurable>],
130 constraints: Constraints,
131 placements: &mut Vec<Placement>,
132 ) -> Size {
133 placements.clear();
134 let density = WearDensity::new(self.density, 1.0);
135 let horizontal = self.horizontal(density);
136 let available = (constraints.max_width - horizontal).max(0.0);
137 let child_constraints = Constraints {
138 min_width: 0.0,
139 max_width: available,
140 min_height: 0.0,
141 max_height: f32::INFINITY,
142 };
143
144 let mut content_width = 0.0f32;
145 let mut content_height = 0.0f32;
146 let mut placeables = Vec::with_capacity(measurables.len());
147 for measurable in measurables {
148 let placeable = measurable.measure(child_constraints);
149 content_width = content_width.max(placeable.width());
150 content_height = content_height.max(placeable.height());
151 placeables.push(placeable);
152 }
153
154 let width = density
157 .ceil(content_width + horizontal)
158 .clamp(constraints.min_width, constraints.max_width);
159 let padded = density.ceil(content_height) + self.vertical(density);
160 let height = density
161 .dp(self.spec.min_height)
162 .max(padded)
163 .clamp(constraints.min_height, constraints.max_height);
164 let surplus = density.centre(height, padded);
168 let top = density.dp(self.spec.padding_top) + surplus;
169
170 for placeable in placeables {
171 let x = density.centre(width, placeable.width());
172 placements.push(Placement::new(placeable.node_id(), x, top, 0));
173 }
174 Size::new(width, height)
175 }
176
177 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
178 let density = WearDensity::new(self.density, 1.0);
179 measurables
180 .iter()
181 .map(|m| m.min_intrinsic_width(height))
182 .fold(0.0, f32::max)
183 + self.horizontal(density)
184 }
185
186 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
187 let density = WearDensity::new(self.density, 1.0);
188 measurables
189 .iter()
190 .map(|m| m.max_intrinsic_width(height))
191 .fold(0.0, f32::max)
192 + self.horizontal(density)
193 }
194
195 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
196 let density = WearDensity::new(self.density, 1.0);
197 let content = measurables
198 .iter()
199 .map(|m| m.min_intrinsic_height(width))
200 .fold(0.0, f32::max);
201 density
202 .dp(self.spec.min_height)
203 .max(density.ceil(content) + self.vertical(density))
204 }
205
206 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
207 self.min_intrinsic_height(measurables, width)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn the_defaults_are_the_ones_the_tokens_declare() {
217 let spec = ListHeaderSpec::default();
218 assert_eq!(spec.min_height, 48.0, "ListHeaderTokens.Height");
219 assert_eq!(spec.padding_start, 14.0);
220 assert_eq!(spec.padding_end, 14.0);
221 assert_eq!(spec.padding_top, 16.0);
222 assert_eq!(spec.padding_bottom, 12.0);
223 assert_eq!(
229 spec.text_style,
230 WearTextStyle::TITLE_MEDIUM.aligned(TextAlign::Center)
231 );
232 }
233}