tessera_ui_basic_components/
glass_slider.rs

1use std::sync::Arc;
2
3use derive_builder::Builder;
4use parking_lot::Mutex;
5use tessera_ui::{
6    Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
7    focus_state::Focus, winit::window::CursorIcon,
8};
9use tessera_ui_macros::tessera;
10
11use crate::{
12    fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
13    shape_def::Shape,
14    surface::{SurfaceArgsBuilder, surface},
15};
16
17/// State for the `glass_slider` component.
18pub struct GlassSliderState {
19    /// True if the user is currently dragging the slider.
20    pub is_dragging: bool,
21    /// The focus handler for the slider.
22    pub focus: Focus,
23}
24
25impl Default for GlassSliderState {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl GlassSliderState {
32    pub fn new() -> Self {
33        Self {
34            is_dragging: false,
35            focus: Focus::new(),
36        }
37    }
38}
39
40/// Arguments for the `glass_slider` component.
41#[derive(Builder, Clone)]
42#[builder(pattern = "owned")]
43pub struct GlassSliderArgs {
44    /// The current value of the slider, ranging from 0.0 to 1.0.
45    #[builder(default = "0.0")]
46    pub value: f32,
47
48    /// Callback function triggered when the slider's value changes.
49    #[builder(default = "Arc::new(|_| {})")]
50    pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
51
52    /// The width of the slider track.
53    #[builder(default = "Dp(200.0)")]
54    pub width: Dp,
55
56    /// The height of the slider track.
57    #[builder(default = "Dp(12.0)")]
58    pub track_height: Dp,
59
60    /// Glass tint color for the track background.
61    #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
62    pub track_tint_color: Color,
63
64    /// Glass tint color for the progress fill.
65    #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
66    pub progress_tint_color: Color,
67
68    /// Glass blur radius for all components.
69    #[builder(default = "8.0")]
70    pub blur_radius: f32,
71
72    /// Border color for the track.
73    #[builder(default = "Color::new(0.5, 0.5, 0.5, 0.3)")]
74    pub track_border_color: Color,
75
76    /// Border width for the track.
77    #[builder(default = "Dp(2.0)")]
78    pub track_border_width: Dp,
79
80    /// Disable interaction.
81    #[builder(default = "false")]
82    pub disabled: bool,
83}
84
85#[tessera]
86pub fn glass_slider(args: impl Into<GlassSliderArgs>, state: Arc<Mutex<GlassSliderState>>) {
87    let args: GlassSliderArgs = args.into();
88
89    // External track (background) with border - capsule shape
90    fluid_glass(
91        FluidGlassArgsBuilder::default()
92            .width(DimensionValue::Fixed(args.width.to_px()))
93            .height(DimensionValue::Fixed(args.track_height.to_px()))
94            .tint_color(args.track_tint_color)
95            .blur_radius(args.blur_radius)
96            .shape(Shape::RoundedRectangle {
97                corner_radius: args.track_height.0 as f32 / 2.0,
98                g2_k_value: 2.0, // Capsule shape
99            })
100            .border(GlassBorder::new(
101                args.track_border_width,
102                args.track_border_color,
103            ))
104            .padding(args.track_border_width)
105            .build()
106            .unwrap(),
107        None,
108        move || {
109            // Internal progress fill - capsule shape using surface
110            let progress_width = (args.width.to_px().to_f32() * args.value)
111                - (args.track_border_width.to_px().to_f32() * 2.0);
112            let effective_height = args.track_height.to_px().to_f32()
113                - (args.track_border_width.to_px().to_f32() * 2.0);
114            surface(
115                SurfaceArgsBuilder::default()
116                    .width(DimensionValue::Fixed(Px(progress_width as i32)))
117                    .height(DimensionValue::Fill {
118                        min: None,
119                        max: None,
120                    })
121                    .color(args.progress_tint_color)
122                    .shape(Shape::RoundedRectangle {
123                        corner_radius: effective_height / 2.0,
124                        g2_k_value: 2.0, // Capsule shape
125                    })
126                    .build()
127                    .unwrap(),
128                None,
129                || {},
130            );
131        },
132    );
133
134    let on_change = args.on_change.clone();
135    let state_handler_state = state.clone();
136    let disabled = args.disabled;
137
138    state_handler(Box::new(move |input| {
139        if disabled {
140            return;
141        }
142        let mut state = state_handler_state.lock();
143
144        let is_in_component = input.cursor_position.is_some_and(|cursor_pos| {
145            cursor_pos.x.0 >= 0
146                && cursor_pos.x.0 < input.computed_data.width.0
147                && cursor_pos.y.0 >= 0
148                && cursor_pos.y.0 < input.computed_data.height.0
149        });
150
151        // Set cursor to pointer when hovering over the slider
152        if is_in_component {
153            input.requests.cursor_icon = CursorIcon::Pointer;
154        }
155
156        if !is_in_component && !state.is_dragging {
157            return;
158        }
159
160        let mut new_value = None;
161
162        for event in input.cursor_events.iter() {
163            match &event.content {
164                CursorEventContent::Pressed(_) => {
165                    state.focus.request_focus();
166                    state.is_dragging = true;
167
168                    if let Some(pos) = input.cursor_position {
169                        let v =
170                            (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
171                        new_value = Some(v);
172                    }
173                }
174                CursorEventContent::Released(_) => {
175                    state.is_dragging = false;
176                }
177                _ => {}
178            }
179        }
180
181        if state.is_dragging {
182            if let Some(pos) = input.cursor_position {
183                let v = (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
184                new_value = Some(v);
185            }
186        }
187
188        if let Some(v) = new_value {
189            if (v - args.value).abs() > f32::EPSILON {
190                on_change(v);
191            }
192        }
193    }));
194
195    measure(Box::new(move |input| {
196        let self_width = args.width.to_px();
197        let self_height = args.track_height.to_px();
198
199        let track_id = input.children_ids[0];
200
201        // Measure track
202        let track_constraint = Constraint::new(
203            DimensionValue::Fixed(self_width),
204            DimensionValue::Fixed(self_height),
205        );
206        tessera_ui::measure_node(
207            track_id,
208            &track_constraint,
209            input.tree,
210            input.metadatas,
211            input.compute_resource_manager.clone(),
212            input.gpu,
213        )?;
214        tessera_ui::place_node(track_id, PxPosition::new(Px(0), Px(0)), input.metadatas);
215
216        Ok(ComputedData {
217            width: self_width,
218            height: self_height,
219        })
220    }));
221}