1use tessera_foundation::gesture::{DragRecognizer, TapRecognizer};
7use tessera_ui::{
8 CallbackWith, Color, ComputedData, Constraint, Dp, FocusProperties, FocusRequester,
9 LayoutResult, MeasurementError, Modifier, PointerInput, PointerInputModifierNode, Px,
10 PxPosition, State,
11 accesskit::Role,
12 layout::{MeasureScope, layout},
13 modifier::{CursorModifierExt as _, FocusModifierExt as _, ModifierCapabilityExt as _},
14 remember, tessera,
15 winit::window::CursorIcon,
16};
17
18use crate::{
19 fluid_glass::{GlassBorder, fluid_glass},
20 glass_progress::glass_progress_fill,
21 modifier::{ModifierExt as _, SemanticsArgs},
22 shape_def::Shape,
23};
24
25const ACCESSIBILITY_STEP: f32 = 0.05;
26
27struct GlassSliderPointerModifierNode {
28 args: GlassSliderConfig,
29 tap_recognizer: State<TapRecognizer>,
30 drag_recognizer: State<DragRecognizer>,
31}
32
33impl PointerInputModifierNode for GlassSliderPointerModifierNode {
34 fn on_pointer_input(&self, mut input: PointerInput<'_>) {
35 if self.args.disabled {
36 return;
37 }
38
39 let controller = self.args.controller;
40 let is_in_component = cursor_within_bounds(input.cursor_position_rel, &input.computed_data);
41
42 if is_in_component || controller.with(|controller| controller.is_dragging()) {
43 let width_f = input.computed_data.width.0 as f32;
44
45 if let Some(value) = process_pointer_gestures(
46 controller,
47 self.tap_recognizer,
48 self.drag_recognizer,
49 &mut input,
50 width_f,
51 ) && (value - self.args.value).abs() > f32::EPSILON
52 {
53 self.args.on_change.call(value);
54 }
55 }
56 }
57}
58
59pub struct GlassSliderController {
61 is_dragging: bool,
62 focus: FocusRequester,
63}
64
65impl GlassSliderController {
66 pub fn new() -> Self {
68 Self {
69 is_dragging: false,
70 focus: FocusRequester::new(),
71 }
72 }
73
74 pub fn is_dragging(&self) -> bool {
76 self.is_dragging
77 }
78
79 pub fn set_dragging(&mut self, dragging: bool) {
81 self.is_dragging = dragging;
82 }
83
84 pub fn request_focus(&mut self) {
86 self.focus.request_focus();
87 }
88
89 pub fn clear_focus(&mut self) {
91 self.focus.unfocus();
92 }
93
94 pub fn is_focused(&self) -> bool {
96 self.focus.is_focused()
97 }
98}
99
100impl Default for GlassSliderController {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106#[derive(Clone, PartialEq)]
107struct GlassSliderConfig {
108 value: f32,
109 modifier: Modifier,
110 on_change: CallbackWith<f32>,
111 track_height: Dp,
112 track_tint_color: Color,
113 progress_tint_color: Color,
114 blur_radius: Dp,
115 track_border_width: Dp,
116 disabled: bool,
117 accessibility_label: Option<String>,
118 accessibility_description: Option<String>,
119 controller: State<GlassSliderController>,
120}
121
122fn cursor_within_bounds(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
123 if let Some(pos) = cursor_pos {
124 let within_x = pos.x >= 0.0 && pos.x < computed.width.0 as f32;
125 let within_y = pos.y >= 0.0 && pos.y < computed.height.0 as f32;
126 within_x && within_y
127 } else {
128 false
129 }
130}
131
132fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
135 cursor_pos.map(|pos| (pos.x / width_f).clamp(0.0, 1.0))
136}
137
138fn process_pointer_gestures(
141 controller: State<GlassSliderController>,
142 tap_recognizer: State<TapRecognizer>,
143 drag_recognizer: State<DragRecognizer>,
144 input: &mut tessera_ui::PointerInput,
145 width_f: f32,
146) -> Option<f32> {
147 let is_in_component = cursor_within_bounds(input.cursor_position_rel, &input.computed_data);
148 let tap_result = tap_recognizer.with_mut(|recognizer| {
149 recognizer.update(
150 input.pass,
151 input.pointer_changes.as_mut_slice(),
152 is_in_component,
153 )
154 });
155 let drag_result = drag_recognizer.with_mut(|recognizer| {
156 recognizer.update(
157 input.pass,
158 input.pointer_changes.as_mut_slice(),
159 is_in_component,
160 )
161 });
162
163 let mut new_value: Option<f32> = None;
164
165 if tap_result.pressed {
166 controller.with_mut(|c| {
167 c.request_focus();
168 });
169 if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
170 new_value = Some(v);
171 }
172 }
173
174 if drag_result.started {
175 controller.with_mut(|c| c.set_dragging(true));
176 }
177
178 if (drag_result.updated || controller.with(|c| c.is_dragging()))
179 && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
180 {
181 new_value = Some(v);
182 }
183
184 if tap_result.released || drag_result.ended {
185 controller.with_mut(|c| c.set_dragging(false));
186 }
187
188 new_value
189}
190
191#[tessera]
231pub fn glass_slider(
232 value: Option<f32>,
233 modifier: Option<Modifier>,
234 on_change: Option<CallbackWith<f32>>,
235 track_height: Option<Dp>,
236 track_tint_color: Option<Color>,
237 progress_tint_color: Option<Color>,
238 blur_radius: Option<Dp>,
239 track_border_width: Option<Dp>,
240 disabled: Option<bool>,
241 #[prop(into)] accessibility_label: Option<String>,
242 #[prop(into)] accessibility_description: Option<String>,
243 controller: Option<State<GlassSliderController>>,
244) {
245 let controller = match controller {
246 Some(controller) => controller,
247 None => remember(GlassSliderController::new),
248 };
249 let slider_args = GlassSliderConfig {
250 value: value.unwrap_or(0.0),
251 modifier: modifier.unwrap_or_else(|| Modifier::new().width(Dp(200.0))),
252 on_change: on_change.unwrap_or_else(CallbackWith::default_value),
253 track_height: track_height.unwrap_or(Dp(12.0)),
254 track_tint_color: track_tint_color.unwrap_or(Color::new(0.3, 0.3, 0.3, 0.15)),
255 progress_tint_color: progress_tint_color.unwrap_or(Color::new(0.5, 0.7, 1.0, 0.25)),
256 blur_radius: blur_radius.unwrap_or(Dp::ZERO),
257 track_border_width: track_border_width.unwrap_or(Dp(1.0)),
258 disabled: disabled.unwrap_or(false),
259 accessibility_label,
260 accessibility_description,
261 controller,
262 };
263 let tap_recognizer = remember(TapRecognizer::default);
264 let drag_recognizer = remember(DragRecognizer::default);
265 render_glass_slider(slider_args, tap_recognizer, drag_recognizer);
266}
267
268#[tessera]
269fn render_glass_slider(
270 args: GlassSliderConfig,
271 tap_recognizer: State<TapRecognizer>,
272 drag_recognizer: State<DragRecognizer>,
273) {
274 let controller = args.controller;
275 let mut modifier = args.modifier.clone();
276 let semantics = SemanticsArgs {
277 role: Some(Role::Slider),
278 label: args.accessibility_label.clone(),
279 description: args.accessibility_description.clone(),
280 numeric_value: Some(args.value as f64),
281 numeric_range: Some((0.0, 1.0)),
282 focusable: !args.disabled,
283 disabled: args.disabled,
284 numeric_value_step: Some(ACCESSIBILITY_STEP as f64),
285 ..Default::default()
286 };
287 modifier = modifier.semantics(semantics);
288 modifier = modifier.then(
289 Modifier::new()
290 .focus_requester(controller.with(|c| c.focus))
291 .focusable()
292 .focus_properties(
293 FocusProperties::new()
294 .can_focus(!args.disabled)
295 .can_request_focus(!args.disabled),
296 ),
297 );
298 if !args.disabled {
299 modifier = modifier.hover_cursor_icon(CursorIcon::Pointer);
300 }
301 let modifier = modifier.push_pointer_input(GlassSliderPointerModifierNode {
302 args: args.clone(),
303 tap_recognizer,
304 drag_recognizer,
305 });
306
307 layout()
308 .modifier(modifier)
309 .policy(GlassSliderLayout {
310 track_height: args.track_height.to_px(),
311 fallback_width: Dp(200.0).to_px(),
312 })
313 .child(move || {
314 fluid_glass()
315 .modifier(Modifier::new().fill_max_size())
316 .tint_color(args.track_tint_color)
317 .blur_radius(args.blur_radius)
318 .shape(Shape::CAPSULE)
319 .border(GlassBorder::new(args.track_border_width.into()))
320 .padding(args.track_border_width)
321 .child(move || {
322 glass_progress_fill()
323 .value(args.value)
324 .tint_color(args.progress_tint_color)
325 .blur_radius(args.blur_radius);
326 });
327 });
328}
329
330#[derive(Clone, Copy, PartialEq)]
331struct GlassSliderLayout {
332 track_height: Px,
333 fallback_width: Px,
334}
335
336impl tessera_ui::LayoutModifierNode for GlassSliderLayout {
337 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
338 let mut result = LayoutResult::default();
339 let self_width = input.parent_constraint().width().clamp(self.fallback_width);
340 let self_height = self.track_height;
341
342 let track = input.children()[0];
343 let track_constraint = Constraint::exact(self_width, self_height);
344 track.measure(input, &track_constraint)?;
345 result.place_child(track, PxPosition::new(Px(0), Px(0)));
346
347 Ok(result.with_size(ComputedData {
348 width: self_width,
349 height: self_height,
350 }))
351 }
352}