tessera_ui_basic_components/
glass_slider.rs1use std::sync::Arc;
15
16use derive_builder::Builder;
17use parking_lot::RwLock;
18use tessera_ui::{
19 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
20 focus_state::Focus, tessera, winit::window::CursorIcon,
21};
22
23use crate::{
24 fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
25 shape_def::Shape,
26};
27
28pub struct GlassSliderState {
30 pub is_dragging: bool,
32 pub focus: Focus,
34}
35
36impl Default for GlassSliderState {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl GlassSliderState {
43 pub fn new() -> Self {
44 Self {
45 is_dragging: false,
46 focus: Focus::new(),
47 }
48 }
49}
50
51#[derive(Builder, Clone)]
53#[builder(pattern = "owned")]
54pub struct GlassSliderArgs {
55 #[builder(default = "0.0")]
57 pub value: f32,
58
59 #[builder(default = "Arc::new(|_| {})")]
61 pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
62
63 #[builder(default = "Dp(200.0)")]
65 pub width: Dp,
66
67 #[builder(default = "Dp(12.0)")]
69 pub track_height: Dp,
70
71 #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
73 pub track_tint_color: Color,
74
75 #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
77 pub progress_tint_color: Color,
78
79 #[builder(default = "8.0")]
81 pub blur_radius: f32,
82
83 #[builder(default = "Dp(1.0)")]
85 pub track_border_width: Dp,
86
87 #[builder(default = "false")]
89 pub disabled: bool,
90}
91
92fn cursor_within_component(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
95 if let Some(pos) = cursor_pos {
96 let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
97 let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
98 within_x && within_y
99 } else {
100 false
101 }
102}
103
104fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
107 cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
108}
109
110fn compute_progress_width(total_width: Px, value: f32, border_padding_px: f32) -> Px {
112 let total_f = total_width.0 as f32;
113 let mut w = total_f * value - border_padding_px;
114 if w < 0.0 {
115 w = 0.0;
116 }
117 Px(w as i32)
118}
119
120fn process_cursor_events(
123 state: &mut GlassSliderState,
124 input: &tessera_ui::InputHandlerInput,
125 width_f: f32,
126) -> Option<f32> {
127 let mut new_value: Option<f32> = None;
128
129 for event in input.cursor_events.iter() {
130 match &event.content {
131 CursorEventContent::Pressed(_) => {
132 state.focus.request_focus();
133 state.is_dragging = true;
134 if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
135 new_value = Some(v);
136 }
137 }
138 CursorEventContent::Released(_) => {
139 state.is_dragging = false;
140 }
141 _ => {}
142 }
143 }
144
145 if state.is_dragging
146 && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
147 {
148 new_value = Some(v);
149 }
150
151 new_value
152}
153
154#[tessera]
198pub fn glass_slider(args: impl Into<GlassSliderArgs>, state: Arc<RwLock<GlassSliderState>>) {
199 let args: GlassSliderArgs = args.into();
200 let border_padding_px = args.track_border_width.to_px().to_f32() * 2.0;
201
202 fluid_glass(
204 FluidGlassArgsBuilder::default()
205 .width(DimensionValue::Fixed(args.width.to_px()))
206 .height(DimensionValue::Fixed(args.track_height.to_px()))
207 .tint_color(args.track_tint_color)
208 .blur_radius(args.blur_radius)
209 .shape({
210 let track_radius_dp = Dp(args.track_height.0 / 2.0);
211 Shape::RoundedRectangle {
212 top_left: track_radius_dp,
213 top_right: track_radius_dp,
214 bottom_right: track_radius_dp,
215 bottom_left: track_radius_dp,
216 g2_k_value: 2.0, }
218 })
219 .border(GlassBorder::new(args.track_border_width.into()))
220 .padding(args.track_border_width)
221 .build()
222 .unwrap(),
223 None,
224 move || {
225 let progress_width_px =
227 compute_progress_width(args.width.to_px(), args.value, border_padding_px);
228 let effective_height = args.track_height.to_px().to_f32() - border_padding_px;
229 fluid_glass(
230 FluidGlassArgsBuilder::default()
231 .width(DimensionValue::Fixed(progress_width_px))
232 .height(DimensionValue::Fill {
233 min: None,
234 max: None,
235 })
236 .tint_color(args.progress_tint_color)
237 .shape({
238 let effective_height_dp = Dp::from_pixels_f32(effective_height);
239 let radius_dp = Dp(effective_height_dp.0 / 2.0);
240 Shape::RoundedRectangle {
241 top_left: radius_dp,
242 top_right: radius_dp,
243 bottom_right: radius_dp,
244 bottom_left: radius_dp,
245 g2_k_value: 2.0, }
247 })
248 .refraction_amount(0.0)
249 .build()
250 .unwrap(),
251 None,
252 || {},
253 );
254 },
255 );
256
257 let on_change = args.on_change.clone();
258 let disabled = args.disabled;
259
260 input_handler(Box::new(move |input| {
261 if disabled {
262 return;
263 }
264
265 let is_in_component =
266 cursor_within_component(input.cursor_position_rel, &input.computed_data);
267
268 if is_in_component {
270 input.requests.cursor_icon = CursorIcon::Pointer;
271 }
272
273 if !is_in_component && !state.read().is_dragging {
274 return;
275 }
276
277 let width_f = input.computed_data.width.0 as f32;
278
279 if let Some(v) = process_cursor_events(&mut state.write(), &input, width_f)
280 && (v - args.value).abs() > f32::EPSILON
281 {
282 on_change(v);
283 }
284 }));
285
286 measure(Box::new(move |input| {
287 let self_width = args.width.to_px();
288 let self_height = args.track_height.to_px();
289
290 let track_id = input.children_ids[0];
291
292 let track_constraint = Constraint::new(
294 DimensionValue::Fixed(self_width),
295 DimensionValue::Fixed(self_height),
296 );
297 input.measure_child(track_id, &track_constraint)?;
298 input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
299
300 Ok(ComputedData {
301 width: self_width,
302 height: self_height,
303 })
304 }));
305}