1use tessera_foundation::gesture::{DragRecognizer, TapRecognizer};
2use tessera_ui::{
3 AccessibilityActionHandler, AccessibilityNode, CallbackWith, ComputedData, FocusRequester,
4 KeyboardInput, KeyboardInputModifierNode, PointerInput, Px, PxPosition, State,
5 accesskit::{Action, Role},
6 winit::{
7 event::ElementState,
8 keyboard::{Key, ModifiersState, NamedKey},
9 },
10};
11
12use super::{
13 ACCESSIBILITY_STEP, SliderArgs, SliderController, SliderLayout, SliderOrientation,
14 axis::SliderAxis,
15};
16
17#[derive(Clone, Copy, PartialEq)]
18pub(super) enum SliderKeyboardModel {
19 Single(State<f32>),
20 Start(State<(f32, f32)>),
21 End(State<(f32, f32)>),
22}
23
24impl SliderKeyboardModel {
25 fn snapshot(self) -> (f32, f32, f32) {
26 match self {
27 Self::Single(value) => (value.get(), 0.0, 1.0),
28 Self::Start(value) => {
29 let (start, end) = value.get();
30 (start, 0.0, end)
31 }
32 Self::End(value) => {
33 let (start, end) = value.get();
34 (end, start, 1.0)
35 }
36 }
37 }
38
39 fn update(self, next: f32) {
40 match self {
41 Self::Single(value) => value.set(next),
42 Self::Start(value) => value.with_mut(|value| value.0 = next),
43 Self::End(value) => value.with_mut(|value| value.1 = next),
44 }
45 }
46}
47
48pub(super) struct SliderKeyboardModifierNode {
49 pub axis: SliderAxis,
50 pub model: SliderKeyboardModel,
51 pub steps: usize,
52 pub disabled: bool,
53 pub space_held: State<bool>,
54 pub on_change: CallbackWith<f32>,
55}
56
57fn key_target(
58 axis: SliderAxis,
59 key: &Key,
60 value: f32,
61 min: f32,
62 max: f32,
63 steps: usize,
64 larger: bool,
65) -> Option<f32> {
66 let intervals = if steps == 0 {
67 100
68 } else {
69 steps.saturating_add(1)
70 };
71 let delta = 1.0 / intervals as f32;
72 let page = (intervals / 10).clamp(1, 10) as f32;
73 let arrow = if larger { page * delta } else { delta };
74 let sign = if axis.reversed { -1.0 } else { 1.0 };
75 let vertical = axis.orientation == SliderOrientation::Vertical;
76 let next = match key {
77 Key::Named(NamedKey::ArrowUp) if vertical => value - sign * arrow,
78 Key::Named(NamedKey::ArrowDown) if vertical => value + sign * arrow,
79 Key::Named(NamedKey::ArrowRight | NamedKey::ArrowLeft) if vertical => return None,
80 Key::Named(NamedKey::ArrowRight) => value + sign * arrow,
81 Key::Named(NamedKey::ArrowLeft) => value - sign * arrow,
82 Key::Named(NamedKey::PageUp) if vertical => value - sign * page * delta,
83 Key::Named(NamedKey::PageDown) if vertical => value + sign * page * delta,
84 Key::Named(NamedKey::ArrowUp) => value + arrow,
85 Key::Named(NamedKey::ArrowDown) => value - arrow,
86 Key::Named(NamedKey::PageUp) => value + page * delta,
87 Key::Named(NamedKey::PageDown) => value - page * delta,
88 Key::Named(NamedKey::Home) => return Some(min),
89 Key::Named(NamedKey::End) => return Some(max),
90 _ => return None,
91 };
92 Some(snap_fraction(next, steps).clamp(min, max))
93}
94
95impl SliderKeyboardModifierNode {
96 fn handle_key(&self, key: &Key, state: ElementState, modifiers: ModifiersState) -> bool {
97 if self.disabled || modifiers.control_key() || modifiers.alt_key() || modifiers.super_key()
98 {
99 return false;
100 }
101 if *key == Key::Named(NamedKey::Space) {
102 let held = state == ElementState::Pressed;
103 if self.space_held.get() != held {
104 self.space_held.set(held);
105 }
106 return true;
107 }
108 let (value, min, max) = self.model.snapshot();
109 let value = if value.is_nan() {
110 min
111 } else {
112 value.clamp(min, max)
113 };
114 let larger = self.space_held.get();
115 let Some(next) = key_target(self.axis, key, value, min, max, self.steps, larger) else {
116 return false;
117 };
118 if state == ElementState::Pressed && (next - value).abs() > f32::EPSILON {
119 self.model.update(next);
120 self.on_change.call(next);
121 }
122 true
123 }
124}
125
126impl KeyboardInputModifierNode for SliderKeyboardModifierNode {
127 fn on_keyboard_input(&self, input: KeyboardInput<'_>) {
128 let modifiers = input.key_modifiers;
129 input
130 .keyboard_events
131 .retain(|event| !self.handle_key(&event.logical_key, event.state, modifiers));
132 }
133}
134
135pub(super) fn snap_fraction(value: f32, steps: usize) -> f32 {
136 let value = if value.is_nan() {
137 0.0
138 } else {
139 value.clamp(0.0, 1.0)
140 };
141 if steps == 0 {
142 return value;
143 }
144 let denom = steps as f32 + 1.0;
145 (value * denom).round() / denom
146}
147
148pub(super) fn cursor_within_bounds(
150 cursor_pos: Option<PxPosition>,
151 computed: &ComputedData,
152) -> bool {
153 if let Some(pos) = cursor_pos {
154 let within_x = pos.x >= 0.0 && pos.x < computed.width.0 as f32;
155 let within_y = pos.y >= 0.0 && pos.y < computed.height.0 as f32;
156 within_x && within_y
157 } else {
158 false
159 }
160}
161
162pub(super) fn cursor_progress(
165 cursor_pos: Option<PxPosition>,
166 layout: &SliderLayout,
167) -> Option<f32> {
168 if layout.track_total_width.0 <= 0 {
169 return None;
170 }
171 cursor_pos.map(|pos| {
172 let cursor_x = layout.axis.input_main(pos, layout.component_width);
173 let half_handle = layout.handle_width.to_f32() / 2.0;
174 let start_x = layout.handle_gap.to_f32() + half_handle;
175 let fraction = (cursor_x - start_x) / layout.track_total_width.to_f32();
176 fraction.clamp(0.0, 1.0)
177 })
178}
179
180fn range_cursor_progress(
181 cursor_pos: Option<PxPosition>,
182 layout: &SliderLayout,
183 start_handle_width: Px,
184 end_handle_width: Px,
185) -> Option<f32> {
186 let cursor_pos = cursor_pos?;
187 let component_width = layout.component_width.to_f32();
188 let gap = layout.handle_gap.to_f32();
189 let start_half = start_handle_width.to_f32() / 2.0;
190 let end_half = end_handle_width.to_f32() / 2.0;
191 let track_total = (component_width - start_half - end_half - gap * 2.0).max(0.0);
192 if track_total <= 0.0 {
193 return None;
194 }
195 let start_x = gap + start_half;
196 let fraction =
197 (layout.axis.input_main(cursor_pos, layout.component_width) - start_x) / track_total;
198 Some(fraction.clamp(0.0, 1.0))
199}
200
201fn range_handle_center_x(
202 layout: &SliderLayout,
203 value: f32,
204 start_handle_width: Px,
205 end_handle_width: Px,
206) -> f32 {
207 let component_width = layout.component_width.to_f32();
208 let gap = layout.handle_gap.to_f32();
209 let start_half = start_handle_width.to_f32() / 2.0;
210 let end_half = end_handle_width.to_f32() / 2.0;
211 let track_total = (component_width - start_half - end_half - gap * 2.0).max(0.0);
212 let start_x = gap + start_half;
213 let raw = start_x + value.clamp(0.0, 1.0) * track_total;
214 raw.clamp(start_x, (component_width - gap - end_half).max(start_x))
215}
216
217pub(super) fn handle_slider_state(
218 input: &mut PointerInput,
219 tap_recognizer: State<TapRecognizer>,
220 drag_recognizer: State<DragRecognizer>,
221 state: State<SliderController>,
222 args: &SliderArgs,
223 layout: &SliderLayout,
224) {
225 if args.disabled {
226 let should_reset = state.with(|inner| inner.is_hovered || inner.is_dragging);
227 if should_reset {
228 state.with_mut(|inner| {
229 inner.is_hovered = false;
230 inner.is_dragging = false;
231 });
232 }
233 return;
234 }
235
236 let is_in_component = cursor_within_bounds(input.cursor_position_rel, &input.computed_data);
237
238 let hover_changed = state.with(|inner| inner.is_hovered != is_in_component);
239 if hover_changed {
240 state.with_mut(|inner| {
241 inner.is_hovered = is_in_component;
242 });
243 }
244
245 let tap_result = tap_recognizer.with_mut(|recognizer| {
246 recognizer.update(
247 input.pass,
248 input.pointer_changes.as_mut_slice(),
249 is_in_component,
250 )
251 });
252 let drag_result = drag_recognizer.with_mut(|recognizer| {
253 recognizer.update(
254 input.pass,
255 input.pointer_changes.as_mut_slice(),
256 is_in_component,
257 )
258 });
259
260 let mut new_value: Option<f32> = None;
261
262 handle_press_for_slider(
263 input,
264 state,
265 &mut new_value,
266 layout,
267 args.steps,
268 tap_result.pressed,
269 );
270 handle_drag_for_slider(
271 input,
272 state,
273 &mut new_value,
274 layout,
275 args.steps,
276 drag_result.started,
277 drag_result.updated,
278 );
279 if tap_result.released || drag_result.ended {
280 state.with_mut(|inner| inner.is_dragging = false);
281 }
282 notify_on_change(new_value, args);
283}
284
285fn handle_press_for_slider(
286 input: &mut PointerInput,
287 state: State<SliderController>,
288 new_value: &mut Option<f32>,
289 layout: &SliderLayout,
290 steps: usize,
291 pressed: bool,
292) {
293 if pressed {
294 state.with_mut(|inner| {
295 inner.focus.request_focus();
296 });
297 if let Some(v) = cursor_progress(input.cursor_position_rel, layout) {
298 *new_value = Some(snap_fraction(v, steps));
299 }
300 }
301}
302
303fn handle_drag_for_slider(
304 input: &PointerInput,
305 state: State<SliderController>,
306 new_value: &mut Option<f32>,
307 layout: &SliderLayout,
308 steps: usize,
309 drag_started: bool,
310 drag_updated: bool,
311) {
312 if drag_started {
313 state.with_mut(|inner| inner.is_dragging = true);
314 }
315
316 if (drag_updated || state.with(|s| s.is_dragging))
317 && let Some(v) = cursor_progress(input.cursor_position_rel, layout)
318 {
319 *new_value = Some(snap_fraction(v, steps));
320 }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
324enum RangeSliderHandle {
325 Start,
326 End,
327}
328
329fn notify_on_change(new_value: Option<f32>, args: &SliderArgs) {
330 if let Some(v) = new_value
331 && (v - args.value).abs() > f32::EPSILON
332 {
333 args.on_change.call(v);
334 }
335}
336
337pub(super) fn apply_slider_semantics(
338 accessibility: &mut AccessibilityNode,
339 action_handler: &mut Option<AccessibilityActionHandler>,
340 args: &SliderArgs,
341 current_value: f32,
342 on_change: &CallbackWith<f32>,
343) {
344 accessibility.role = Some(Role::Slider);
345 accessibility.orientation = Some(args.axis.accessibility_orientation());
346 accessibility.label = args.accessibility_label.clone();
347 accessibility.description = args.accessibility_description.clone();
348 accessibility.numeric_value = Some(current_value as f64);
349 accessibility.value = args.value_label.clone().or_else(|| {
350 (args.value_indicator != super::SliderValueIndicatorVisibility::Hidden)
351 .then(|| format!("{}%", super::default_value_label(current_value)))
352 });
353 accessibility.min_numeric_value = Some(0.0);
354 accessibility.max_numeric_value = Some(1.0);
355 accessibility.focusable = !args.disabled;
356 accessibility.disabled = args.disabled;
357 accessibility.actions.clear();
358
359 if args.disabled {
360 *action_handler = None;
361 return;
362 }
363
364 let on_change = *on_change;
365 let steps = args.steps;
366 *action_handler = Some(Box::new(move |action| {
367 let delta = if steps == 0 {
368 ACCESSIBILITY_STEP
369 } else {
370 1.0 / (steps as f32 + 1.0)
371 };
372 let new_value = match action {
373 Action::Increment => Some(snap_fraction(current_value + delta, steps)),
374 Action::Decrement => Some(snap_fraction(current_value - delta, steps)),
375 _ => None,
376 };
377
378 if let Some(new_value) = new_value
379 && (new_value - current_value).abs() > f32::EPSILON
380 {
381 on_change.call(new_value);
382 }
383 }));
384 accessibility.actions.push(Action::Increment);
385 accessibility.actions.push(Action::Decrement);
386}
387
388pub struct RangeSliderController {
390 pub(crate) is_hovered: bool,
391 pub(crate) is_dragging_start: bool,
392 pub(crate) is_dragging_end: bool,
393 active_handle: Option<RangeSliderHandle>,
394 pub(crate) focus_start: FocusRequester,
395 pub(crate) focus_end: FocusRequester,
396}
397
398impl Default for RangeSliderController {
399 fn default() -> Self {
400 Self::new()
401 }
402}
403
404impl RangeSliderController {
405 pub fn new() -> Self {
407 Self {
408 is_hovered: false,
409 is_dragging_start: false,
410 is_dragging_end: false,
411 active_handle: None,
412 focus_start: FocusRequester::new(),
413 focus_end: FocusRequester::new(),
414 }
415 }
416}
417
418#[derive(Clone, Copy)]
419pub(super) struct RangeSliderHandleWidths {
420 pub start: Px,
421 pub end: Px,
422}
423
424pub(super) fn handle_range_slider_state(
425 input: &mut PointerInput,
426 tap_recognizer: State<TapRecognizer>,
427 drag_recognizer: State<DragRecognizer>,
428 state: &State<RangeSliderController>,
429 args: &super::RangeSliderArgs,
430 layout: &SliderLayout,
431 handle_widths: RangeSliderHandleWidths,
432) {
433 if args.disabled {
434 let should_reset = state
435 .with(|inner| inner.is_hovered || inner.is_dragging_start || inner.is_dragging_end);
436 if should_reset {
437 state.with_mut(|inner| {
438 inner.is_hovered = false;
439 inner.is_dragging_start = false;
440 inner.is_dragging_end = false;
441 inner.active_handle = None;
442 });
443 }
444 return;
445 }
446
447 let is_in_component = cursor_within_bounds(input.cursor_position_rel, &input.computed_data);
448
449 let hover_changed = state.with(|inner| inner.is_hovered != is_in_component);
450 if hover_changed {
451 state.with_mut(|inner| {
452 inner.is_hovered = is_in_component;
453 });
454 }
455
456 let tap_result = tap_recognizer.with_mut(|recognizer| {
457 recognizer.update(
458 input.pass,
459 input.pointer_changes.as_mut_slice(),
460 is_in_component,
461 )
462 });
463 let drag_result = drag_recognizer.with_mut(|recognizer| {
464 recognizer.update(
465 input.pass,
466 input.pointer_changes.as_mut_slice(),
467 is_in_component,
468 )
469 });
470
471 let mut new_start: Option<f32> = None;
472 let mut new_end: Option<f32> = None;
473
474 if tap_result.pressed
475 && let Some(progress) = range_cursor_progress(
476 input.cursor_position_rel,
477 layout,
478 handle_widths.start,
479 handle_widths.end,
480 )
481 {
482 let progress = snap_fraction(progress, args.steps);
483 let active_handle = choose_range_slider_handle(
484 input.cursor_position_rel,
485 layout,
486 args.value.0.clamp(0.0, 1.0),
487 args.value.1.clamp(args.value.0.clamp(0.0, 1.0), 1.0),
488 handle_widths.start,
489 handle_widths.end,
490 );
491 state.with_mut(|inner| {
492 inner.active_handle = Some(active_handle);
493 match active_handle {
494 RangeSliderHandle::Start => inner.focus_start.request_focus(),
495 RangeSliderHandle::End => inner.focus_end.request_focus(),
496 }
497 });
498 match active_handle {
499 RangeSliderHandle::Start => new_start = Some(progress.min(args.value.1)),
500 RangeSliderHandle::End => new_end = Some(progress.max(args.value.0)),
501 }
502 }
503
504 if drag_result.started {
505 state.with_mut(|inner| match inner.active_handle {
506 Some(RangeSliderHandle::Start) => inner.is_dragging_start = true,
507 Some(RangeSliderHandle::End) => inner.is_dragging_end = true,
508 None => {}
509 });
510 }
511
512 if (drag_result.updated || state.with(|s| s.is_dragging_start || s.is_dragging_end))
513 && let Some(progress) = range_cursor_progress(
514 input.cursor_position_rel,
515 layout,
516 handle_widths.start,
517 handle_widths.end,
518 )
519 {
520 let progress = snap_fraction(progress, args.steps);
521 state.with(|s| {
522 if s.is_dragging_start {
523 new_start = Some(progress.min(args.value.1)); } else if s.is_dragging_end {
525 new_end = Some(progress.max(args.value.0)); }
527 });
528 }
529
530 if tap_result.released || drag_result.ended {
531 state.with_mut(|inner| {
532 inner.is_dragging_start = false;
533 inner.is_dragging_end = false;
534 inner.active_handle = None;
535 });
536 }
537
538 if let Some(ns) = new_start
539 && (ns - args.value.0).abs() > f32::EPSILON
540 {
541 args.on_change.call((ns, args.value.1));
542 }
543 if let Some(ne) = new_end
544 && (ne - args.value.1).abs() > f32::EPSILON
545 {
546 args.on_change.call((args.value.0, ne));
547 }
548}
549
550fn choose_range_slider_handle(
551 cursor_pos: Option<PxPosition>,
552 layout: &SliderLayout,
553 start_value: f32,
554 end_value: f32,
555 start_handle_width: Px,
556 end_handle_width: Px,
557) -> RangeSliderHandle {
558 let cursor_x = cursor_pos.map(|pos| layout.axis.input_main(pos, layout.component_width));
559 let start_center_x =
560 range_handle_center_x(layout, start_value, start_handle_width, end_handle_width);
561 let end_center_x =
562 range_handle_center_x(layout, end_value, start_handle_width, end_handle_width);
563 let dist_start = cursor_x.map(|x| (x - start_center_x).abs());
564 let dist_end = cursor_x.map(|x| (x - end_center_x).abs());
565 if dist_start.unwrap_or(f32::INFINITY) <= dist_end.unwrap_or(f32::INFINITY) {
566 RangeSliderHandle::Start
567 } else {
568 RangeSliderHandle::End
569 }
570}
571
572pub(super) fn apply_range_slider_semantics(
573 accessibility: &mut AccessibilityNode,
574 args: &super::RangeSliderArgs,
575 _current_start: f32,
576 _current_end: f32,
577 _on_change: &CallbackWith<(f32, f32)>,
578) {
579 accessibility.role = Some(Role::Group);
580 accessibility.label = args.accessibility_label.clone();
581 accessibility.description = args.accessibility_description.clone();
582 accessibility.hidden = false;
583 accessibility.focusable = false;
584 accessibility.merge_descendants = true;
585 accessibility.disabled = args.disabled;
586}
587
588#[cfg(test)]
589mod tests {
590 use std::sync::Arc;
591
592 use parking_lot::Mutex;
593
594 use super::*;
595
596 #[test]
597 fn snapped_values_match_tick_fractions_and_preserve_exact_endpoints() {
598 for (value, steps, expected) in [
599 (1.0, 40, 1.0),
600 (0.0, 40, 0.0),
601 (0.87, 6, 0.85714287),
602 (0.72, 6, 0.71428573),
603 (0.34, 2, 0.33333334),
604 (0.66, 2, 0.6666667),
605 (0.625, 3, 0.75),
606 (f32::NAN, 0, 0.0),
607 (f32::INFINITY, 6, 1.0),
608 (f32::NEG_INFINITY, 6, 0.0),
609 ] {
610 assert_eq!(snap_fraction(value, steps), expected);
611 }
612 }
613
614 #[test]
615 fn continuous_and_discrete_keys_follow_their_step_and_page_sizes() {
616 for (key, expected) in [
617 (NamedKey::ArrowRight, 0.51),
618 (NamedKey::ArrowUp, 0.51),
619 (NamedKey::ArrowLeft, 0.49),
620 (NamedKey::ArrowDown, 0.49),
621 (NamedKey::Home, 0.0),
622 (NamedKey::End, 1.0),
623 (NamedKey::PageUp, 0.6),
624 (NamedKey::PageDown, 0.4),
625 ] {
626 assert!(
627 (key_target(
628 SliderAxis::default(),
629 &Key::Named(key),
630 0.5,
631 0.0,
632 1.0,
633 0,
634 false
635 )
636 .unwrap()
637 - expected)
638 .abs()
639 < 0.000001
640 );
641 }
642 for (steps, next, page) in [(3, 0.75, 0.75), (19, 0.55, 0.6), (199, 0.505, 0.55)] {
643 assert!(
644 (key_target(
645 SliderAxis::default(),
646 &Key::Named(NamedKey::ArrowRight),
647 0.5,
648 0.0,
649 1.0,
650 steps,
651 false
652 )
653 .unwrap()
654 - next)
655 .abs()
656 < 0.000001
657 );
658 assert!(
659 (key_target(
660 SliderAxis::default(),
661 &Key::Named(NamedKey::PageUp),
662 0.5,
663 0.0,
664 1.0,
665 steps,
666 false
667 )
668 .unwrap()
669 - page)
670 .abs()
671 < 0.000001
672 );
673 }
674 }
675
676 #[test]
677 fn range_keys_observe_the_neighbor_without_rescaling_the_step() {
678 assert_eq!(
679 key_target(
680 SliderAxis::default(),
681 &Key::Named(NamedKey::Home),
682 0.5,
683 0.2,
684 0.8,
685 0,
686 false
687 ),
688 Some(0.2)
689 );
690 assert_eq!(
691 key_target(
692 SliderAxis::default(),
693 &Key::Named(NamedKey::End),
694 0.5,
695 0.2,
696 0.8,
697 0,
698 false
699 ),
700 Some(0.8)
701 );
702 assert!(
703 (key_target(
704 SliderAxis::default(),
705 &Key::Named(NamedKey::ArrowRight),
706 0.5,
707 0.2,
708 0.8,
709 0,
710 false
711 )
712 .unwrap()
713 - 0.51)
714 .abs()
715 < 0.000001
716 );
717 assert_eq!(
718 key_target(
719 SliderAxis::default(),
720 &Key::Named(NamedKey::ArrowRight),
721 0.75,
722 0.0,
723 0.8,
724 3,
725 false
726 ),
727 Some(0.8)
728 );
729 assert_eq!(
730 key_target(
731 SliderAxis::default(),
732 &Key::Named(NamedKey::ArrowLeft),
733 0.25,
734 0.2,
735 1.0,
736 3,
737 false
738 ),
739 Some(0.2)
740 );
741 }
742
743 #[test]
744 fn vertical_keys_follow_screen_direction_without_consuming_horizontal_arrows() {
745 for reversed in [false, true] {
746 let axis = SliderAxis {
747 orientation: SliderOrientation::Vertical,
748 reversed,
749 };
750 for (key, expected) in [
751 (NamedKey::ArrowUp, if reversed { 0.75 } else { 0.25 }),
752 (NamedKey::ArrowDown, if reversed { 0.25 } else { 0.75 }),
753 (NamedKey::PageUp, if reversed { 0.75 } else { 0.25 }),
754 (NamedKey::PageDown, if reversed { 0.25 } else { 0.75 }),
755 (NamedKey::Home, 0.0),
756 (NamedKey::End, 1.0),
757 ] {
758 assert_eq!(
759 key_target(axis, &Key::Named(key), 0.5, 0.0, 1.0, 3, false),
760 Some(expected)
761 );
762 }
763 for key in [NamedKey::ArrowLeft, NamedKey::ArrowRight, NamedKey::Tab] {
764 assert_eq!(
765 key_target(axis, &Key::Named(key), 0.5, 0.0, 1.0, 3, false),
766 None
767 );
768 }
769 }
770 let reversed = SliderAxis {
771 reversed: true,
772 ..Default::default()
773 };
774 assert_eq!(
775 key_target(
776 reversed,
777 &Key::Named(NamedKey::ArrowRight),
778 0.5,
779 0.0,
780 1.0,
781 3,
782 false
783 ),
784 Some(0.25)
785 );
786 assert_eq!(
787 key_target(
788 reversed,
789 &Key::Named(NamedKey::ArrowLeft),
790 0.5,
791 0.0,
792 1.0,
793 3,
794 false
795 ),
796 Some(0.75)
797 );
798 }
799
800 #[tessera_ui::tessera]
801 fn key_dispatch_fixture() {
802 let changes = Arc::new(Mutex::new(Vec::new()));
803 let recorded = changes.clone();
804 let value = tessera_ui::remember(|| 0.5);
805 let mut node = SliderKeyboardModifierNode {
806 axis: SliderAxis::default(),
807 model: SliderKeyboardModel::Single(value),
808 steps: 0,
809 disabled: false,
810 space_held: tessera_ui::remember(|| false),
811 on_change: CallbackWith::new(move |value| recorded.lock().push(value)),
812 };
813 assert!(node.handle_key(
814 &Key::Named(NamedKey::ArrowRight),
815 ElementState::Released,
816 ModifiersState::empty()
817 ));
818 assert!(changes.lock().is_empty());
819 for modifiers in [
820 ModifiersState::CONTROL,
821 ModifiersState::ALT,
822 ModifiersState::SUPER,
823 ] {
824 assert!(!node.handle_key(&Key::Named(NamedKey::End), ElementState::Pressed, modifiers));
825 }
826 for key in [Key::Named(NamedKey::Tab), Key::Character("q".into())] {
827 assert!(!node.handle_key(&key, ElementState::Pressed, ModifiersState::empty()));
828 }
829 for _ in 0..3 {
830 assert!(node.handle_key(
831 &Key::Named(NamedKey::ArrowRight),
832 ElementState::Pressed,
833 ModifiersState::empty()
834 ));
835 }
836 assert_eq!(changes.lock().len(), 3);
837 assert!((value.get() - 0.53).abs() < 0.000001);
838 let space = Key::Named(NamedKey::Space);
839 assert!(node.handle_key(&space, ElementState::Pressed, ModifiersState::empty()));
840 assert!(node.handle_key(
841 &Key::Named(NamedKey::ArrowRight),
842 ElementState::Pressed,
843 ModifiersState::empty()
844 ));
845 assert!((value.get() - 0.63).abs() < 0.000001);
846 assert!(node.handle_key(&space, ElementState::Released, ModifiersState::empty()));
847 assert!(node.handle_key(
848 &Key::Named(NamedKey::ArrowLeft),
849 ElementState::Pressed,
850 ModifiersState::empty()
851 ));
852 assert!((value.get() - 0.62).abs() < 0.000001);
853 assert_eq!(changes.lock().len(), 5);
854 node.handle_key(
855 &Key::Named(NamedKey::End),
856 ElementState::Pressed,
857 ModifiersState::empty(),
858 );
859 assert!(node.handle_key(
860 &Key::Named(NamedKey::ArrowRight),
861 ElementState::Pressed,
862 ModifiersState::empty()
863 ));
864 assert_eq!(changes.lock().len(), 6);
865 node.disabled = true;
866 assert!(!node.handle_key(
867 &Key::Named(NamedKey::Home),
868 ElementState::Pressed,
869 ModifiersState::empty()
870 ));
871 assert_eq!(value.get(), 1.0);
872 let range = tessera_ui::remember(|| (0.2, 0.8));
873 let start = SliderKeyboardModifierNode {
874 axis: SliderAxis::default(),
875 model: SliderKeyboardModel::Start(range),
876 steps: 0,
877 disabled: false,
878 space_held: tessera_ui::remember(|| false),
879 on_change: CallbackWith::new(move |_| recorded_range(range)),
880 };
881 let end = SliderKeyboardModifierNode {
882 axis: SliderAxis::default(),
883 model: SliderKeyboardModel::End(range),
884 steps: 0,
885 disabled: false,
886 space_held: tessera_ui::remember(|| false),
887 on_change: CallbackWith::new(move |_| recorded_range(range)),
888 };
889 for key in [
890 NamedKey::ArrowRight,
891 NamedKey::ArrowRight,
892 NamedKey::ArrowRight,
893 ] {
894 assert!(start.handle_key(
895 &Key::Named(key),
896 ElementState::Pressed,
897 ModifiersState::empty(),
898 ));
899 }
900 assert!((range.get().0 - 0.23).abs() < 0.000001);
901 end.handle_key(
902 &Key::Named(NamedKey::Home),
903 ElementState::Pressed,
904 ModifiersState::empty(),
905 );
906 assert_eq!(range.get().0, range.get().1);
907 start.handle_key(
908 &Key::Named(NamedKey::End),
909 ElementState::Pressed,
910 ModifiersState::empty(),
911 );
912 assert_eq!(range.get().0, range.get().1);
913 end.handle_key(
914 &Key::Named(NamedKey::End),
915 ElementState::Pressed,
916 ModifiersState::empty(),
917 );
918 start.handle_key(
919 &Key::Named(NamedKey::End),
920 ElementState::Pressed,
921 ModifiersState::empty(),
922 );
923 assert_eq!(range.get(), (1.0, 1.0));
924 crate::spacer::spacer();
925 }
926
927 fn recorded_range(range: State<(f32, f32)>) {
928 let (start, end) = range.get();
929 assert!(start <= end);
930 }
931
932 #[test]
933 fn key_dispatch_consumes_limits_and_releases_but_preserves_shortcuts_and_tab() {
934 tessera_ui::testing::layout_test(|| {
935 key_dispatch_fixture();
936 })
937 .run();
938 }
939}