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)]
58pub struct GlassSliderState {
59 inner: Arc<RwLock<GlassSliderStateInner>>,
60}
61
62impl GlassSliderState {
63 pub fn new() -> Self {
65 Self {
66 inner: Arc::new(RwLock::new(GlassSliderStateInner::new())),
67 }
68 }
69
70 pub(crate) fn read(&self) -> RwLockReadGuard<'_, GlassSliderStateInner> {
71 self.inner.read()
72 }
73
74 pub(crate) fn write(&self) -> RwLockWriteGuard<'_, GlassSliderStateInner> {
75 self.inner.write()
76 }
77
78 pub fn is_dragging(&self) -> bool {
80 self.inner.read().is_dragging
81 }
82
83 pub fn set_dragging(&self, dragging: bool) {
85 self.inner.write().is_dragging = dragging;
86 }
87
88 pub fn request_focus(&self) {
90 self.inner.write().focus.request_focus();
91 }
92
93 pub fn clear_focus(&self) {
95 self.inner.write().focus.unfocus();
96 }
97
98 pub fn is_focused(&self) -> bool {
100 self.inner.read().focus.is_focused()
101 }
102}
103
104impl Default for GlassSliderState {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110#[derive(Builder, Clone)]
112#[builder(pattern = "owned")]
113pub struct GlassSliderArgs {
114 #[builder(default = "0.0")]
116 pub value: f32,
117
118 #[builder(default = "Arc::new(|_| {})")]
120 pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
121
122 #[builder(default = "Dp(200.0)")]
124 pub width: Dp,
125
126 #[builder(default = "Dp(12.0)")]
128 pub track_height: Dp,
129
130 #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
132 pub track_tint_color: Color,
133
134 #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
136 pub progress_tint_color: Color,
137
138 #[builder(default = "Dp(0.0)")]
140 pub blur_radius: Dp,
141
142 #[builder(default = "Dp(1.0)")]
144 pub track_border_width: Dp,
145
146 #[builder(default = "false")]
148 pub disabled: bool,
149 #[builder(default, setter(strip_option, into))]
151 pub accessibility_label: Option<String>,
152 #[builder(default, setter(strip_option, into))]
154 pub accessibility_description: Option<String>,
155}
156
157fn cursor_within_component(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
160 if let Some(pos) = cursor_pos {
161 let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
162 let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
163 within_x && within_y
164 } else {
165 false
166 }
167}
168
169fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
172 cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
173}
174
175fn compute_progress_width(total_width: Px, value: f32, border_padding_px: f32) -> Px {
177 let total_f = total_width.0 as f32;
178 let mut w = total_f * value - border_padding_px;
179 if w < 0.0 {
180 w = 0.0;
181 }
182 Px(w as i32)
183}
184
185fn process_cursor_events(
188 state: &GlassSliderState,
189 input: &tessera_ui::InputHandlerInput,
190 width_f: f32,
191) -> Option<f32> {
192 let mut new_value: Option<f32> = None;
193
194 for event in input.cursor_events.iter() {
195 match &event.content {
196 CursorEventContent::Pressed(_) => {
197 {
198 let mut inner = state.write();
199 inner.focus.request_focus();
200 inner.is_dragging = true;
201 }
202 if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
203 new_value = Some(v);
204 }
205 }
206 CursorEventContent::Released(_) => {
207 state.write().is_dragging = false;
208 }
209 _ => {}
210 }
211 }
212
213 if state.read().is_dragging
214 && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
215 {
216 new_value = Some(v);
217 }
218
219 new_value
220}
221
222#[tessera]
268pub fn glass_slider(args: impl Into<GlassSliderArgs>, state: GlassSliderState) {
269 let args: GlassSliderArgs = args.into();
270 let border_padding_px = args.track_border_width.to_px().to_f32() * 2.0;
271
272 fluid_glass(
274 FluidGlassArgsBuilder::default()
275 .width(DimensionValue::Fixed(args.width.to_px()))
276 .height(DimensionValue::Fixed(args.track_height.to_px()))
277 .tint_color(args.track_tint_color)
278 .blur_radius(args.blur_radius)
279 .shape(Shape::capsule())
280 .border(GlassBorder::new(args.track_border_width.into()))
281 .padding(args.track_border_width)
282 .build()
283 .expect("builder construction failed"),
284 None,
285 move || {
286 let progress_width_px =
288 compute_progress_width(args.width.to_px(), args.value, 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(Shape::capsule())
298 .refraction_amount(0.0)
299 .build()
300 .expect("builder construction failed"),
301 None,
302 || {},
303 );
304 },
305 );
306
307 let on_change = args.on_change.clone();
308 let args_for_handler = args.clone();
309 let state_for_handler = state.clone();
310 input_handler(Box::new(move |mut input| {
311 if !args_for_handler.disabled {
312 let is_in_component =
313 cursor_within_component(input.cursor_position_rel, &input.computed_data);
314
315 if is_in_component {
316 input.requests.cursor_icon = CursorIcon::Pointer;
317 }
318
319 if is_in_component || state_for_handler.read().is_dragging {
320 let width_f = input.computed_data.width.0 as f32;
321
322 if let Some(v) = process_cursor_events(&state_for_handler, &input, width_f)
323 && (v - args_for_handler.value).abs() > f32::EPSILON
324 {
325 on_change(v);
326 }
327 }
328 }
329
330 apply_glass_slider_accessibility(
331 &mut input,
332 &args_for_handler,
333 args_for_handler.value,
334 &args_for_handler.on_change,
335 );
336 }));
337
338 measure(Box::new(move |input| {
339 let self_width = args.width.to_px();
340 let self_height = args.track_height.to_px();
341
342 let track_id = input.children_ids[0];
343
344 let track_constraint = Constraint::new(
346 DimensionValue::Fixed(self_width),
347 DimensionValue::Fixed(self_height),
348 );
349 input.measure_child(track_id, &track_constraint)?;
350 input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
351
352 Ok(ComputedData {
353 width: self_width,
354 height: self_height,
355 })
356 }));
357}
358
359fn apply_glass_slider_accessibility(
360 input: &mut tessera_ui::InputHandlerInput<'_>,
361 args: &GlassSliderArgs,
362 current_value: f32,
363 on_change: &Arc<dyn Fn(f32) + Send + Sync>,
364) {
365 let mut builder = input.accessibility().role(Role::Slider);
366
367 if let Some(label) = args.accessibility_label.as_ref() {
368 builder = builder.label(label.clone());
369 }
370 if let Some(description) = args.accessibility_description.as_ref() {
371 builder = builder.description(description.clone());
372 }
373
374 builder = builder
375 .numeric_value(current_value as f64)
376 .numeric_range(0.0, 1.0);
377
378 if args.disabled {
379 builder = builder.disabled();
380 } else {
381 builder = builder
382 .action(Action::Increment)
383 .action(Action::Decrement)
384 .focusable();
385 }
386
387 builder.commit();
388
389 if args.disabled {
390 return;
391 }
392
393 let value_for_handler = current_value;
394 let on_change = on_change.clone();
395 input.set_accessibility_action_handler(move |action| {
396 let new_value = match action {
397 Action::Increment => Some((value_for_handler + ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
398 Action::Decrement => Some((value_for_handler - ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
399 _ => None,
400 };
401
402 if let Some(new_value) = new_value
403 && (new_value - value_for_handler).abs() > f32::EPSILON
404 {
405 on_change(new_value);
406 }
407 });
408}