tessera_ui_basic_components/
glass_progress.rs

1//! A progress bar with a glassmorphic visual style.
2//!
3//! ## Usage
4//!
5//! Use to indicate the completion of a task or a specific value in a range.
6use derive_builder::Builder;
7use tessera_ui::{Color, ComputedData, Constraint, DimensionValue, Dp, Px, PxPosition, tessera};
8
9use crate::{
10    fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
11    shape_def::{RoundedCorner, Shape},
12};
13
14/// Arguments for the `glass_progress` component.
15#[derive(Builder, Clone, Debug)]
16#[builder(pattern = "owned")]
17pub struct GlassProgressArgs {
18    /// The current value of the progress bar, ranging from 0.0 to 1.0.
19    #[builder(default = "0.0")]
20    pub value: f32,
21
22    /// The width of the progress bar.
23    #[builder(default = "DimensionValue::Fixed(Dp(200.0).to_px())")]
24    pub width: DimensionValue,
25
26    /// The height of the progress bar.
27    #[builder(default = "Dp(12.0)")]
28    pub height: Dp,
29
30    /// Glass tint color for the track background.
31    #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
32    pub track_tint_color: Color,
33
34    /// Glass tint color for the progress fill.
35    #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
36    pub progress_tint_color: Color,
37
38    /// Glass blur radius for all components.
39    #[builder(default = "Dp(8.0)")]
40    pub blur_radius: Dp,
41
42    /// Border width for the track.
43    #[builder(default = "Dp(1.0)")]
44    pub track_border_width: Dp,
45}
46
47/// Produce a capsule-shaped RoundedRectangle shape for the given height (px).
48fn capsule_shape_for_height(height: Dp) -> Shape {
49    let radius = Dp(height.0 / 2.0);
50    Shape::RoundedRectangle {
51        top_left: RoundedCorner::manual(radius, 2.0),
52        top_right: RoundedCorner::manual(radius, 2.0),
53        bottom_right: RoundedCorner::manual(radius, 2.0),
54        bottom_left: RoundedCorner::manual(radius, 2.0),
55    }
56}
57
58/// Compute progress width and inner effective height (excluding borders).
59/// Returns None when progress width is zero or negative.
60fn compute_progress_dims(args: &GlassProgressArgs, width_px: Px) -> Option<(Px, f32)> {
61    let progress_width = (width_px.to_f32() * args.value.clamp(0.0, 1.0))
62        - (args.track_border_width.to_px().to_f32() * 2.0);
63    let effective_height =
64        args.height.to_px().to_f32() - (args.track_border_width.to_px().to_f32() * 2.0);
65
66    if progress_width > 0.0 {
67        Some((Px(progress_width as i32), effective_height))
68    } else {
69        None
70    }
71}
72
73fn resolve_width_px(args: &GlassProgressArgs, parent: Option<&Constraint>) -> Px {
74    let fallback = Dp(200.0).to_px();
75    let base = Constraint::new(args.width, DimensionValue::Fixed(args.height.to_px()));
76    let merged = match parent {
77        Some(parent_constraint) => base.merge(parent_constraint),
78        None => base,
79    };
80
81    match merged.width {
82        DimensionValue::Fixed(px) => px,
83        DimensionValue::Fill { max, .. } | DimensionValue::Wrap { max, .. } => {
84            max.unwrap_or(fallback)
85        }
86    }
87}
88
89/// Render the outer track and the inner progress fill.
90/// Extracted to reduce the size of `glass_progress` and keep each unit focused.
91fn render_track_and_fill(args: GlassProgressArgs, width_px: Px) {
92    fluid_glass(
93        FluidGlassArgsBuilder::default()
94            .width(DimensionValue::Fixed(width_px))
95            .height(DimensionValue::Fixed(args.height.to_px()))
96            .tint_color(args.track_tint_color)
97            .blur_radius(args.blur_radius)
98            .shape(capsule_shape_for_height(args.height))
99            .border(GlassBorder::new(args.track_border_width.into()))
100            .padding(args.track_border_width)
101            .build()
102            .expect("builder construction failed"),
103        None,
104        move || {
105            // Internal progress fill - capsule shape
106            if let Some((progress_px, effective_height)) = compute_progress_dims(&args, width_px) {
107                fluid_glass(
108                    FluidGlassArgsBuilder::default()
109                        .width(DimensionValue::Fixed(progress_px))
110                        .height(DimensionValue::Fill {
111                            min: None,
112                            max: None,
113                        })
114                        .tint_color(args.progress_tint_color)
115                        .shape(capsule_shape_for_height(Dp::from_pixels_f32(
116                            effective_height,
117                        )))
118                        .refraction_amount(0.0)
119                        .build()
120                        .expect("builder construction failed"),
121                    None,
122                    || {},
123                );
124            }
125        },
126    );
127}
128
129/// # glass_progress
130///
131/// Renders a progress bar with a customizable glass effect.
132///
133/// ## Usage
134///
135/// Display a value in a continuous range (0.0 to 1.0) with a modern, glass-like appearance.
136///
137/// ## Parameters
138///
139/// - `args` — configures the progress bar's value and appearance; see [`GlassProgressArgs`].
140///
141/// ## Examples
142///
143/// ```
144/// use tessera_ui_basic_components::glass_progress::{glass_progress, GlassProgressArgsBuilder};
145///
146/// // Render a progress bar at 75% completion.
147/// glass_progress(
148///     GlassProgressArgsBuilder::default()
149///         .value(0.75)
150///         .build()
151///         .unwrap(),
152/// );
153/// ```
154#[tessera]
155pub fn glass_progress(args: impl Into<GlassProgressArgs>) {
156    let args: GlassProgressArgs = args.into();
157    let fallback_width = resolve_width_px(&args, None);
158
159    // Render track and inner fill using extracted helper.
160    let args_for_render = args.clone();
161    render_track_and_fill(args_for_render, fallback_width);
162
163    measure(Box::new(move |input| {
164        let self_width = resolve_width_px(&args, Some(input.parent_constraint));
165        let self_height = args.height.to_px();
166
167        let track_id = input.children_ids[0];
168
169        // Measure track
170        let track_constraint = Constraint::new(
171            DimensionValue::Fixed(self_width),
172            DimensionValue::Fixed(self_height),
173        );
174        input.measure_child(track_id, &track_constraint)?;
175        input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
176
177        Ok(ComputedData {
178            width: self_width,
179            height: self_height,
180        })
181    }));
182}