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::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 = "Dp(200.0)")]
24    pub width: Dp,
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: radius,
52        top_right: radius,
53        bottom_right: radius,
54        bottom_left: radius,
55        g2_k_value: 2.0,
56    }
57}
58
59/// Compute progress width and inner effective height (excluding borders).
60/// Returns None when progress width is zero or negative.
61fn compute_progress_dims(args: &GlassProgressArgs) -> Option<(Px, f32)> {
62    let progress_width = (args.width.to_px().to_f32() * args.value.clamp(0.0, 1.0))
63        - (args.track_border_width.to_px().to_f32() * 2.0);
64    let effective_height =
65        args.height.to_px().to_f32() - (args.track_border_width.to_px().to_f32() * 2.0);
66
67    if progress_width > 0.0 {
68        Some((Px(progress_width as i32), effective_height))
69    } else {
70        None
71    }
72}
73
74/// Render the outer track and the inner progress fill.
75/// Extracted to reduce the size of `glass_progress` and keep each unit focused.
76fn render_track_and_fill(args: GlassProgressArgs) {
77    fluid_glass(
78        FluidGlassArgsBuilder::default()
79            .width(DimensionValue::Fixed(args.width.to_px()))
80            .height(DimensionValue::Fixed(args.height.to_px()))
81            .tint_color(args.track_tint_color)
82            .blur_radius(args.blur_radius)
83            .shape(capsule_shape_for_height(args.height))
84            .border(GlassBorder::new(args.track_border_width.into()))
85            .padding(args.track_border_width)
86            .build()
87            .unwrap(),
88        None,
89        move || {
90            // Internal progress fill - capsule shape
91            if let Some((progress_px, effective_height)) = compute_progress_dims(&args) {
92                fluid_glass(
93                    FluidGlassArgsBuilder::default()
94                        .width(DimensionValue::Fixed(progress_px))
95                        .height(DimensionValue::Fill {
96                            min: None,
97                            max: None,
98                        })
99                        .tint_color(args.progress_tint_color)
100                        .shape(capsule_shape_for_height(Dp::from_pixels_f32(
101                            effective_height,
102                        )))
103                        .refraction_amount(0.0)
104                        .build()
105                        .unwrap(),
106                    None,
107                    || {},
108                );
109            }
110        },
111    );
112}
113
114/// # glass_progress
115///
116/// Renders a progress bar with a customizable glass effect.
117///
118/// ## Usage
119///
120/// Display a value in a continuous range (0.0 to 1.0) with a modern, glass-like appearance.
121///
122/// ## Parameters
123///
124/// - `args` — configures the progress bar's value and appearance; see [`GlassProgressArgs`].
125///
126/// ## Examples
127///
128/// ```
129/// use tessera_ui_basic_components::glass_progress::{glass_progress, GlassProgressArgsBuilder};
130///
131/// // Render a progress bar at 75% completion.
132/// glass_progress(
133///     GlassProgressArgsBuilder::default()
134///         .value(0.75)
135///         .build()
136///         .unwrap(),
137/// );
138/// ```
139#[tessera]
140pub fn glass_progress(args: impl Into<GlassProgressArgs>) {
141    let args: GlassProgressArgs = args.into();
142
143    // Render track and inner fill using extracted helper.
144    let args_for_render = args.clone();
145    render_track_and_fill(args_for_render);
146
147    measure(Box::new(move |input| {
148        let self_width = args.width.to_px();
149        let self_height = args.height.to_px();
150
151        let track_id = input.children_ids[0];
152
153        // Measure track
154        let track_constraint = Constraint::new(
155            DimensionValue::Fixed(self_width),
156            DimensionValue::Fixed(self_height),
157        );
158        input.measure_child(track_id, &track_constraint)?;
159        input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
160
161        Ok(ComputedData {
162            width: self_width,
163            height: self_height,
164        })
165    }));
166}