tessera_ui_basic_components/slider/
interaction.rs1use std::sync::Arc;
2
3use tessera_ui::{
4 ComputedData, CursorEventContent, InputHandlerInput, PxPosition,
5 accesskit::{Action, Role},
6 winit::window::CursorIcon,
7};
8
9use super::{ACCESSIBILITY_STEP, SliderArgs, SliderLayout, SliderState};
10
11pub(super) fn cursor_within_component(
13 cursor_pos: Option<PxPosition>,
14 computed: &ComputedData,
15) -> bool {
16 if let Some(pos) = cursor_pos {
17 let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
18 let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
19 within_x && within_y
20 } else {
21 false
22 }
23}
24
25pub(super) fn cursor_progress(
28 cursor_pos: Option<PxPosition>,
29 layout: &SliderLayout,
30) -> Option<f32> {
31 if layout.component_width.0 <= 0 {
32 return None;
33 }
34 cursor_pos.map(|pos| (pos.x.0 as f32 / layout.component_width.to_f32()).clamp(0.0, 1.0))
35}
36
37pub(super) fn handle_slider_state(
38 input: &mut InputHandlerInput,
39 state: &SliderState,
40 args: &SliderArgs,
41 layout: &SliderLayout,
42) {
43 if args.disabled {
44 let mut inner = state.write();
45 inner.is_hovered = false;
46 inner.is_dragging = false;
47 return;
48 }
49
50 let is_in_component = cursor_within_component(input.cursor_position_rel, &input.computed_data);
51
52 {
53 let mut inner = state.write();
54 inner.is_hovered = is_in_component;
55 }
56
57 if is_in_component {
58 input.requests.cursor_icon = CursorIcon::Pointer;
59 }
60
61 if !is_in_component && !state.read().is_dragging {
62 return;
63 }
64
65 let mut new_value: Option<f32> = None;
66
67 handle_cursor_events(input, state, &mut new_value, layout);
68 update_value_on_drag(input, state, &mut new_value, layout);
69 notify_on_change(new_value, args);
70}
71
72fn handle_cursor_events(
73 input: &mut InputHandlerInput,
74 state: &SliderState,
75 new_value: &mut Option<f32>,
76 layout: &SliderLayout,
77) {
78 for event in input.cursor_events.iter() {
79 match &event.content {
80 CursorEventContent::Pressed(_) => {
81 {
82 let mut inner = state.write();
83 inner.focus.request_focus();
84 inner.is_dragging = true;
85 }
86 if let Some(v) = cursor_progress(input.cursor_position_rel, layout) {
87 *new_value = Some(v);
88 }
89 }
90 CursorEventContent::Released(_) => {
91 state.write().is_dragging = false;
92 }
93 _ => {}
94 }
95 }
96}
97
98fn update_value_on_drag(
99 input: &InputHandlerInput,
100 state: &SliderState,
101 new_value: &mut Option<f32>,
102 layout: &SliderLayout,
103) {
104 if state.read().is_dragging
105 && let Some(v) = cursor_progress(input.cursor_position_rel, layout)
106 {
107 *new_value = Some(v);
108 }
109}
110
111fn notify_on_change(new_value: Option<f32>, args: &SliderArgs) {
112 if let Some(v) = new_value
113 && (v - args.value).abs() > f32::EPSILON
114 {
115 (args.on_change)(v);
116 }
117}
118
119pub(super) fn apply_slider_accessibility(
120 input: &mut InputHandlerInput<'_>,
121 args: &SliderArgs,
122 current_value: f32,
123 on_change: &Arc<dyn Fn(f32) + Send + Sync>,
124) {
125 let mut builder = input.accessibility().role(Role::Slider);
126
127 if let Some(label) = args.accessibility_label.as_ref() {
128 builder = builder.label(label.clone());
129 }
130 if let Some(description) = args.accessibility_description.as_ref() {
131 builder = builder.description(description.clone());
132 }
133
134 builder = builder
135 .numeric_value(current_value as f64)
136 .numeric_range(0.0, 1.0);
137
138 if args.disabled {
139 builder = builder.disabled();
140 } else {
141 builder = builder
142 .focusable()
143 .action(Action::Increment)
144 .action(Action::Decrement);
145 }
146
147 builder.commit();
148
149 if args.disabled {
150 return;
151 }
152
153 let value_for_handler = current_value;
154 let on_change = on_change.clone();
155 input.set_accessibility_action_handler(move |action| {
156 let new_value = match action {
157 Action::Increment => Some((value_for_handler + ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
158 Action::Decrement => Some((value_for_handler - ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
159 _ => None,
160 };
161
162 if let Some(new_value) = new_value
163 && (new_value - value_for_handler).abs() > f32::EPSILON
164 {
165 on_change(new_value);
166 }
167 });
168}
169
170pub(crate) struct RangeSliderStateInner {
171 pub is_dragging_start: bool,
172 pub is_dragging_end: bool,
173 pub focus_start: tessera_ui::focus_state::Focus,
174 pub focus_end: tessera_ui::focus_state::Focus,
175 pub is_hovered: bool,
176}
177
178impl Default for RangeSliderStateInner {
179 fn default() -> Self {
180 Self {
181 is_dragging_start: false,
182 is_dragging_end: false,
183 focus_start: tessera_ui::focus_state::Focus::new(),
184 focus_end: tessera_ui::focus_state::Focus::new(),
185 is_hovered: false,
186 }
187 }
188}
189
190#[derive(Clone)]
192pub struct RangeSliderState {
193 inner: Arc<parking_lot::RwLock<RangeSliderStateInner>>,
194}
195
196impl Default for RangeSliderState {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl RangeSliderState {
203 pub fn new() -> Self {
205 Self {
206 inner: Arc::new(parking_lot::RwLock::new(RangeSliderStateInner::default())),
207 }
208 }
209
210 pub(crate) fn read(&self) -> parking_lot::RwLockReadGuard<'_, RangeSliderStateInner> {
211 self.inner.read()
212 }
213
214 pub(crate) fn write(&self) -> parking_lot::RwLockWriteGuard<'_, RangeSliderStateInner> {
215 self.inner.write()
216 }
217}
218
219pub(super) fn handle_range_slider_state(
220 input: &mut InputHandlerInput,
221 state: &RangeSliderState,
222 args: &super::RangeSliderArgs,
223 layout: &SliderLayout,
224) {
225 if args.disabled {
226 let mut inner = state.write();
227 inner.is_hovered = false;
228 inner.is_dragging_start = false;
229 inner.is_dragging_end = false;
230 return;
231 }
232
233 let is_in_component = cursor_within_component(input.cursor_position_rel, &input.computed_data);
234
235 {
236 let mut inner = state.write();
237 inner.is_hovered = is_in_component;
238 }
239
240 if is_in_component {
241 input.requests.cursor_icon = CursorIcon::Pointer;
242 }
243
244 let is_dragging = {
245 let r = state.read();
246 r.is_dragging_start || r.is_dragging_end
247 };
248
249 if !is_in_component && !is_dragging {
250 return;
251 }
252
253 let mut new_start: Option<f32> = None;
254 let mut new_end: Option<f32> = None;
255
256 for event in input.cursor_events.iter() {
257 match &event.content {
258 CursorEventContent::Pressed(_) => {
259 if let Some(progress) = cursor_progress(input.cursor_position_rel, layout) {
260 let dist_start = (progress - args.value.0).abs();
261 let dist_end = (progress - args.value.1).abs();
262
263 let mut inner = state.write();
264 if dist_start <= dist_end {
266 inner.is_dragging_start = true;
267 inner.focus_start.request_focus();
268 new_start = Some(progress);
269 } else {
270 inner.is_dragging_end = true;
271 inner.focus_end.request_focus();
272 new_end = Some(progress);
273 }
274 }
275 }
276 CursorEventContent::Released(_) => {
277 let mut inner = state.write();
278 inner.is_dragging_start = false;
279 inner.is_dragging_end = false;
280 }
281 _ => {}
282 }
283 }
284
285 let r = state.read();
286 if (r.is_dragging_start || r.is_dragging_end)
287 && let Some(progress) = cursor_progress(input.cursor_position_rel, layout)
288 {
289 if r.is_dragging_start {
290 new_start = Some(progress.min(args.value.1)); } else {
292 new_end = Some(progress.max(args.value.0)); }
294 }
295 drop(r);
296
297 if let Some(ns) = new_start
298 && (ns - args.value.0).abs() > f32::EPSILON
299 {
300 (args.on_change)((ns, args.value.1));
301 }
302 if let Some(ne) = new_end
303 && (ne - args.value.1).abs() > f32::EPSILON
304 {
305 (args.on_change)((args.value.0, ne));
306 }
307}
308
309pub(super) fn apply_range_slider_accessibility(
310 input: &mut InputHandlerInput<'_>,
311 args: &super::RangeSliderArgs,
312 _current_start: f32,
313 _current_end: f32,
314 _on_change: &Arc<dyn Fn((f32, f32)) + Send + Sync>,
315) {
316 let mut builder = input.accessibility().role(Role::Slider);
329 if let Some(label) = args.accessibility_label.as_ref() {
330 builder = builder.label(label.clone());
331 }
332 if args.disabled {
333 builder = builder.disabled();
334 }
335 builder.commit();
336}