tessera_ui_basic_components/
glass_slider.rs1use std::sync::Arc;
7
8use derive_builder::Builder;
9use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
10use tessera_ui::{
11 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
12 accesskit::{Action, Role},
13 focus_state::Focus,
14 tessera,
15 winit::window::CursorIcon,
16};
17
18use crate::{
19 fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
20 shape_def::Shape,
21};
22
23const ACCESSIBILITY_STEP: f32 = 0.05;
24
25pub(crate) struct GlassSliderStateInner {
27 pub is_dragging: bool,
29 pub focus: Focus,
31}
32
33impl Default for GlassSliderStateInner {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl GlassSliderStateInner {
40 pub fn new() -> Self {
41 Self {
42 is_dragging: false,
43 focus: Focus::new(),
44 }
45 }
46}
47
48#[derive(Clone)]
49pub struct GlassSliderState {
50 inner: Arc<RwLock<GlassSliderStateInner>>,
51}
52
53impl GlassSliderState {
54 pub fn new() -> Self {
55 Self {
56 inner: Arc::new(RwLock::new(GlassSliderStateInner::new())),
57 }
58 }
59
60 pub(crate) fn read(&self) -> RwLockReadGuard<'_, GlassSliderStateInner> {
61 self.inner.read()
62 }
63
64 pub(crate) fn write(&self) -> RwLockWriteGuard<'_, GlassSliderStateInner> {
65 self.inner.write()
66 }
67
68 pub fn is_dragging(&self) -> bool {
70 self.inner.read().is_dragging
71 }
72
73 pub fn set_dragging(&self, dragging: bool) {
75 self.inner.write().is_dragging = dragging;
76 }
77
78 pub fn request_focus(&self) {
80 self.inner.write().focus.request_focus();
81 }
82
83 pub fn clear_focus(&self) {
85 self.inner.write().focus.unfocus();
86 }
87
88 pub fn is_focused(&self) -> bool {
90 self.inner.read().focus.is_focused()
91 }
92}
93
94impl Default for GlassSliderState {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100#[derive(Builder, Clone)]
102#[builder(pattern = "owned")]
103pub struct GlassSliderArgs {
104 #[builder(default = "0.0")]
106 pub value: f32,
107
108 #[builder(default = "Arc::new(|_| {})")]
110 pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
111
112 #[builder(default = "Dp(200.0)")]
114 pub width: Dp,
115
116 #[builder(default = "Dp(12.0)")]
118 pub track_height: Dp,
119
120 #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
122 pub track_tint_color: Color,
123
124 #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
126 pub progress_tint_color: Color,
127
128 #[builder(default = "Dp(0.0)")]
130 pub blur_radius: Dp,
131
132 #[builder(default = "Dp(1.0)")]
134 pub track_border_width: Dp,
135
136 #[builder(default = "false")]
138 pub disabled: bool,
139 #[builder(default, setter(strip_option, into))]
141 pub accessibility_label: Option<String>,
142 #[builder(default, setter(strip_option, into))]
144 pub accessibility_description: Option<String>,
145}
146
147fn cursor_within_component(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
150 if let Some(pos) = cursor_pos {
151 let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
152 let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
153 within_x && within_y
154 } else {
155 false
156 }
157}
158
159fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
162 cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
163}
164
165fn compute_progress_width(total_width: Px, value: f32, border_padding_px: f32) -> Px {
167 let total_f = total_width.0 as f32;
168 let mut w = total_f * value - border_padding_px;
169 if w < 0.0 {
170 w = 0.0;
171 }
172 Px(w as i32)
173}
174
175fn process_cursor_events(
178 state: &GlassSliderState,
179 input: &tessera_ui::InputHandlerInput,
180 width_f: f32,
181) -> Option<f32> {
182 let mut new_value: Option<f32> = None;
183
184 for event in input.cursor_events.iter() {
185 match &event.content {
186 CursorEventContent::Pressed(_) => {
187 {
188 let mut inner = state.write();
189 inner.focus.request_focus();
190 inner.is_dragging = true;
191 }
192 if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
193 new_value = Some(v);
194 }
195 }
196 CursorEventContent::Released(_) => {
197 state.write().is_dragging = false;
198 }
199 _ => {}
200 }
201 }
202
203 if state.read().is_dragging
204 && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
205 {
206 new_value = Some(v);
207 }
208
209 new_value
210}
211
212#[tessera]
258pub fn glass_slider(args: impl Into<GlassSliderArgs>, state: GlassSliderState) {
259 let args: GlassSliderArgs = args.into();
260 let border_padding_px = args.track_border_width.to_px().to_f32() * 2.0;
261
262 fluid_glass(
264 FluidGlassArgsBuilder::default()
265 .width(DimensionValue::Fixed(args.width.to_px()))
266 .height(DimensionValue::Fixed(args.track_height.to_px()))
267 .tint_color(args.track_tint_color)
268 .blur_radius(args.blur_radius)
269 .shape({
270 let track_radius_dp = Dp(args.track_height.0 / 2.0);
271 Shape::RoundedRectangle {
272 top_left: track_radius_dp,
273 top_right: track_radius_dp,
274 bottom_right: track_radius_dp,
275 bottom_left: track_radius_dp,
276 g2_k_value: 2.0, }
278 })
279 .border(GlassBorder::new(args.track_border_width.into()))
280 .padding(args.track_border_width)
281 .build()
282 .unwrap(),
283 None,
284 move || {
285 let progress_width_px =
287 compute_progress_width(args.width.to_px(), args.value, border_padding_px);
288 let effective_height = args.track_height.to_px().to_f32() - border_padding_px;
289 fluid_glass(
290 FluidGlassArgsBuilder::default()
291 .width(DimensionValue::Fixed(progress_width_px))
292 .height(DimensionValue::Fill {
293 min: None,
294 max: None,
295 })
296 .tint_color(args.progress_tint_color)
297 .shape({
298 let effective_height_dp = Dp::from_pixels_f32(effective_height);
299 let radius_dp = Dp(effective_height_dp.0 / 2.0);
300 Shape::RoundedRectangle {
301 top_left: radius_dp,
302 top_right: radius_dp,
303 bottom_right: radius_dp,
304 bottom_left: radius_dp,
305 g2_k_value: 2.0, }
307 })
308 .refraction_amount(0.0)
309 .build()
310 .unwrap(),
311 None,
312 || {},
313 );
314 },
315 );
316
317 let on_change = args.on_change.clone();
318 let args_for_handler = args.clone();
319 let state_for_handler = state.clone();
320 input_handler(Box::new(move |mut input| {
321 if !args_for_handler.disabled {
322 let is_in_component =
323 cursor_within_component(input.cursor_position_rel, &input.computed_data);
324
325 if is_in_component {
326 input.requests.cursor_icon = CursorIcon::Pointer;
327 }
328
329 if is_in_component || state_for_handler.read().is_dragging {
330 let width_f = input.computed_data.width.0 as f32;
331
332 if let Some(v) = process_cursor_events(&state_for_handler, &input, width_f)
333 && (v - args_for_handler.value).abs() > f32::EPSILON
334 {
335 on_change(v);
336 }
337 }
338 }
339
340 apply_glass_slider_accessibility(
341 &mut input,
342 &args_for_handler,
343 args_for_handler.value,
344 &args_for_handler.on_change,
345 );
346 }));
347
348 measure(Box::new(move |input| {
349 let self_width = args.width.to_px();
350 let self_height = args.track_height.to_px();
351
352 let track_id = input.children_ids[0];
353
354 let track_constraint = Constraint::new(
356 DimensionValue::Fixed(self_width),
357 DimensionValue::Fixed(self_height),
358 );
359 input.measure_child(track_id, &track_constraint)?;
360 input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
361
362 Ok(ComputedData {
363 width: self_width,
364 height: self_height,
365 })
366 }));
367}
368
369fn apply_glass_slider_accessibility(
370 input: &mut tessera_ui::InputHandlerInput<'_>,
371 args: &GlassSliderArgs,
372 current_value: f32,
373 on_change: &Arc<dyn Fn(f32) + Send + Sync>,
374) {
375 let mut builder = input.accessibility().role(Role::Slider);
376
377 if let Some(label) = args.accessibility_label.as_ref() {
378 builder = builder.label(label.clone());
379 }
380 if let Some(description) = args.accessibility_description.as_ref() {
381 builder = builder.description(description.clone());
382 }
383
384 builder = builder
385 .numeric_value(current_value as f64)
386 .numeric_range(0.0, 1.0);
387
388 if args.disabled {
389 builder = builder.disabled();
390 } else {
391 builder = builder
392 .action(Action::Increment)
393 .action(Action::Decrement)
394 .focusable();
395 }
396
397 builder.commit();
398
399 if args.disabled {
400 return;
401 }
402
403 let value_for_handler = current_value;
404 let on_change = on_change.clone();
405 input.set_accessibility_action_handler(move |action| {
406 let new_value = match action {
407 Action::Increment => Some((value_for_handler + ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
408 Action::Decrement => Some((value_for_handler - ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
409 _ => None,
410 };
411
412 if let Some(new_value) = new_value
413 && (new_value - value_for_handler).abs() > f32::EPSILON
414 {
415 on_change(new_value);
416 }
417 });
418}