cranpose_ui/widgets/wear/
list_header.rs1use cranpose_core::NodeId;
25use cranpose_ui_graphics::Size;
26use cranpose_ui_layout::{
27 Constraints, Measurable, MeasurePolicy, MeasureResult, MeasureScope, Placement,
28};
29
30use crate::{
31 composable,
32 density::Density,
33 modifier::Modifier,
34 text::paragraph::TextAlign,
35 widgets::{
36 Layout, Text,
37 wear::theme::{WearColors, WearTextStyle},
38 },
39};
40
41#[derive(Clone, Copy, Debug, PartialEq)]
43pub struct ListHeaderSpec {
44 pub colors: WearColors,
45 pub text_style: WearTextStyle,
46 pub min_height: f32,
48 pub padding_start: f32,
49 pub padding_top: f32,
50 pub padding_end: f32,
51 pub padding_bottom: f32,
52}
53
54impl Default for ListHeaderSpec {
55 fn default() -> Self {
56 Self {
57 colors: WearColors::default(),
58 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: crate::density::density().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: Density) -> f32 {
108 density.dp(self.spec.padding_start) + density.dp(self.spec.padding_end)
109 }
110
111 fn vertical(&self, density: Density) -> 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 scope: &dyn MeasureScope,
120 measurables: &[Box<dyn Measurable>],
121 constraints: Constraints,
122 ) -> MeasureResult {
123 let mut placements = Vec::new();
124 let measurement = self.measure_into(scope, measurables, constraints, &mut placements);
125 MeasureResult::new(measurement, placements)
126 }
127
128 fn measure_into(
129 &self,
130 _scope: &dyn MeasureScope,
131 measurables: &[Box<dyn Measurable>],
132 constraints: Constraints,
133 placements: &mut Vec<Placement>,
134 ) -> cranpose_ui_layout::Measurement {
135 placements.clear();
136 let density = Density::new(self.density, 1.0);
137 let horizontal = self.horizontal(density);
138 let available = (constraints.max_width - horizontal).max(0.0);
139 let child_constraints = Constraints {
140 min_width: 0.0,
141 max_width: available,
142 min_height: 0.0,
143 max_height: f32::INFINITY,
144 };
145
146 let mut content_width = 0.0f32;
147 let mut content_height = 0.0f32;
148 let mut placeables = Vec::with_capacity(measurables.len());
149 for measurable in measurables {
150 let placeable = measurable.measure(child_constraints);
151 content_width = content_width.max(placeable.width());
152 content_height = content_height.max(placeable.height());
153 placeables.push(placeable);
154 }
155
156 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);
165 let top = density.dp(self.spec.padding_top) + surplus;
166
167 for placeable in placeables {
168 let x = density.centre(width, placeable.width());
169 placements.push(Placement::new(placeable.node_id(), x, top, 0));
170 }
171 Size::new(width, height).into()
172 }
173
174 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
175 let density = Density::new(self.density, 1.0);
176 measurables
177 .iter()
178 .map(|m| m.min_intrinsic_width(height))
179 .fold(0.0, f32::max)
180 + self.horizontal(density)
181 }
182
183 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
184 let density = Density::new(self.density, 1.0);
185 measurables
186 .iter()
187 .map(|m| m.max_intrinsic_width(height))
188 .fold(0.0, f32::max)
189 + self.horizontal(density)
190 }
191
192 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
193 let density = Density::new(self.density, 1.0);
194 let content = measurables
195 .iter()
196 .map(|m| m.min_intrinsic_height(width))
197 .fold(0.0, f32::max);
198 density
199 .dp(self.spec.min_height)
200 .max(density.ceil(content) + self.vertical(density))
201 }
202
203 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
204 self.min_intrinsic_height(measurables, width)
205 }
206}
207
208#[cfg(test)]
209#[path = "tests/list_header_tests.rs"]
210mod tests;