1use tessera_ui::{
7 Color, ComputedData, Constraint, Dp, LayoutResult, MeasurementError, Modifier, Px, PxPosition,
8 layout::{MeasureScope, layout},
9 tessera,
10};
11
12use crate::{
13 fluid_glass::{GlassBorder, fluid_glass},
14 modifier::ModifierExt as _,
15 shape_def::{RoundedCorner, Shape},
16};
17
18fn default_progress_modifier() -> Modifier {
19 Modifier::new().width(Dp(200.0))
20}
21
22fn capsule_shape_for_height(height: Dp) -> Shape {
24 let radius = Dp(height.0 / 2.0);
25 Shape::RoundedRectangle {
26 top_left: RoundedCorner::manual(radius, 2.0),
27 top_right: RoundedCorner::manual(radius, 2.0),
28 bottom_right: RoundedCorner::manual(radius, 2.0),
29 bottom_left: RoundedCorner::manual(radius, 2.0),
30 }
31}
32
33#[tessera]
34pub(crate) fn glass_progress_fill(
35 value: Option<f32>,
36 tint_color: Option<Color>,
37 blur_radius: Option<Dp>,
38 shape: Option<Shape>,
39) {
40 let value = value.unwrap_or(0.0);
41 let tint_color = tint_color.unwrap_or(Color::TRANSPARENT);
42 let blur_radius = blur_radius.unwrap_or(Dp(0.0));
43 let shape = shape.unwrap_or(Shape::CAPSULE);
44 let value = value.clamp(0.0, 1.0);
45 layout()
46 .policy(GlassProgressFillLayout { value })
47 .child(move || {
48 fluid_glass()
49 .tint_color(tint_color)
50 .blur_radius(blur_radius)
51 .shape(shape)
52 .child(|| {});
53 });
54}
55
56#[derive(Clone, PartialEq)]
57struct GlassProgressFillLayout {
58 value: f32,
59}
60
61impl tessera_ui::LayoutModifierNode for GlassProgressFillLayout {
62 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
63 let mut result = LayoutResult::default();
64 let available_width = input
65 .parent_constraint()
66 .width()
67 .resolve_max()
68 .unwrap_or(Px(0));
69 let available_height = input
70 .parent_constraint()
71 .height()
72 .resolve_max()
73 .unwrap_or(Px(0));
74
75 let width_px = Px((available_width.to_f32() * self.value).round() as i32);
76 let child = input
77 .children()
78 .first()
79 .copied()
80 .expect("progress fill child should exist");
81
82 let child_constraint = Constraint::exact(width_px, available_height);
83 child.measure(input, &child_constraint)?;
84 result.place_child(child, PxPosition::new(Px(0), Px(0)));
85
86 Ok(result.with_size(ComputedData {
87 width: width_px,
88 height: available_height,
89 }))
90 }
91}
92
93#[tessera]
126pub fn glass_progress(
127 value: Option<f32>,
128 modifier: Option<Modifier>,
129 height: Option<Dp>,
130 track_tint_color: Option<Color>,
131 progress_tint_color: Option<Color>,
132 blur_radius: Option<Dp>,
133 track_border_width: Option<Dp>,
134) {
135 let value = value.unwrap_or(0.0);
136 let modifier = modifier.unwrap_or_else(default_progress_modifier);
137 let height = height.unwrap_or(Dp(12.0));
138 let track_tint_color = track_tint_color.unwrap_or(Color::new(0.3, 0.3, 0.3, 0.15));
139 let progress_tint_color = progress_tint_color.unwrap_or(Color::new(0.5, 0.7, 1.0, 0.25));
140 let blur_radius = blur_radius.unwrap_or(Dp(8.0));
141 let track_border_width = track_border_width.unwrap_or(Dp(1.0));
142 let height_px = height.to_px();
143 layout()
144 .modifier(modifier)
145 .policy(GlassProgressLayout { height: height_px })
146 .child(move || {
147 let effective_height = Dp((height.0 - (track_border_width.0 * 2.0)).max(0.0));
148 let fill_shape = capsule_shape_for_height(effective_height);
149
150 fluid_glass()
151 .tint_color(track_tint_color)
152 .blur_radius(blur_radius)
153 .shape(capsule_shape_for_height(height))
154 .border(GlassBorder::new(track_border_width.into()))
155 .padding(track_border_width)
156 .child(move || {
157 glass_progress_fill()
158 .value(value)
159 .tint_color(progress_tint_color)
160 .blur_radius(blur_radius)
161 .shape(fill_shape);
162 });
163 });
164}
165
166#[derive(Clone, Copy, PartialEq)]
167struct GlassProgressLayout {
168 height: Px,
169}
170
171impl tessera_ui::LayoutModifierNode for GlassProgressLayout {
172 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
173 let mut result = LayoutResult::default();
174 let track = input
175 .children()
176 .first()
177 .copied()
178 .expect("track should exist");
179 let constraint = Constraint::new(input.parent_constraint().width(), self.height);
180 let track_measurement = track.measure(input, &constraint)?;
181 result.place_child(track, PxPosition::new(Px(0), Px(0)));
182 Ok(result.with_size(track_measurement.size()))
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use tessera_ui::testing::{layout_test, pump_layout_test_session, start_layout_test_session};
189
190 use crate::{column::column, glass_slider::glass_slider};
191
192 use super::*;
193
194 #[tessera]
195 fn controls(value: f32) {
196 column().children(move || {
197 glass_slider()
198 .modifier(Modifier::new().width(Dp(200.0)))
199 .track_height(Dp(20.0))
200 .track_border_width(Dp::ZERO)
201 .value(value);
202 glass_progress()
203 .modifier(Modifier::new().width(Dp(200.0)))
204 .height(Dp(20.0))
205 .track_border_width(Dp::ZERO)
206 .value(value);
207 });
208 }
209
210 #[test]
211 fn glass_controls_measure_their_painted_fill_from_clamped_progress() {
212 for (value, width) in [(-0.5, 0), (0.25, 50), (0.75, 150), (1.5, 200)] {
213 let mut session = start_layout_test_session(
214 layout_test(move || {
215 controls(value);
216 })
217 .viewport_px(220, 60),
218 );
219 pump_layout_test_session(&mut session, 0);
220 let snapshot = session.inspect_components();
221 let sizes: Vec<_> = snapshot
222 .nodes()
223 .iter()
224 .filter(|node| {
225 std::iter::successors(snapshot.parent(node), |parent| snapshot.parent(parent))
226 .take_while(|parent| parent.layout_coordinates().is_none())
227 .any(|parent| parent.name() == "fluid_glass_inner")
228 })
229 .filter_map(|node| {
230 node.layout_coordinates()
231 .map(|coordinates| coordinates.local_size)
232 })
233 .collect();
234 assert_eq!(
235 sizes,
236 [
237 tessera_ui::PxSize::new(Px(200), Px(20)),
238 tessera_ui::PxSize::new(Px(width), Px(20)),
239 tessera_ui::PxSize::new(Px(200), Px(20)),
240 tessera_ui::PxSize::new(Px(width), Px(20)),
241 ]
242 );
243 }
244 }
245}