1use crate::widget::Id;
3use crate::widget::operation::accessible::Accessible;
4use crate::widget::operation::scrollable::{AbsoluteOffset, RelativeOffset, Scrollable};
5use crate::widget::operation::{self, Operation, Outcome};
6use crate::{Rectangle, Vector};
7
8pub trait Focusable {
10 fn is_focused(&self) -> bool;
12
13 fn focus(&mut self);
15
16 fn unfocus(&mut self);
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub struct Count {
23 pub focused: Option<usize>,
25
26 pub total: usize,
28}
29
30pub fn focus<T>(target: Id) -> impl Operation<T> {
32 struct Focus {
33 target: Id,
34 }
35
36 impl<T> Operation<T> for Focus {
37 fn focusable(&mut self, id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
38 match id {
39 Some(id) if id == &self.target => {
40 state.focus();
41 }
42 _ => {
43 state.unfocus();
44 }
45 }
46 }
47
48 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
49 operate(self);
50 }
51 }
52
53 Focus { target }
54}
55
56pub fn unfocus<T>() -> impl Operation<T> {
58 struct Unfocus;
59
60 impl<T> Operation<T> for Unfocus {
61 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
62 state.unfocus();
63 }
64
65 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
66 operate(self);
67 }
68 }
69
70 Unfocus
71}
72
73pub fn count() -> impl Operation<Count> {
76 struct CountFocusable {
77 count: Count,
78 }
79
80 impl Operation<Count> for CountFocusable {
81 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
82 if state.is_focused() {
83 self.count.focused = Some(self.count.total);
84 }
85
86 self.count.total += 1;
87 }
88
89 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Count>)) {
90 operate(self);
91 }
92
93 fn finish(&self) -> Outcome<Count> {
94 Outcome::Some(self.count)
95 }
96 }
97
98 CountFocusable {
99 count: Count::default(),
100 }
101}
102
103struct ScopedCountResult {
105 target: Id,
106 count: Count,
107}
108
109fn scoped_count(target: Id) -> impl Operation<ScopedCountResult> {
115 struct ScopedCount {
116 target: Id,
117 pending_scope: bool,
118 inside_scope: bool,
119 count: Count,
120 }
121
122 impl Operation<ScopedCountResult> for ScopedCount {
123 fn container(&mut self, id: Option<&Id>, _bounds: Rectangle) {
124 if id.is_some_and(|id| *id == self.target) {
125 self.pending_scope = true;
126 }
127 }
128
129 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<ScopedCountResult>)) {
130 let was_inside = self.inside_scope;
131 if self.pending_scope {
132 self.inside_scope = true;
133 self.pending_scope = false;
134 }
135 operate(self);
136 self.inside_scope = was_inside;
137 }
138
139 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
140 if !self.inside_scope {
141 return;
142 }
143
144 if state.is_focused() {
145 self.count.focused = Some(self.count.total);
146 }
147
148 self.count.total += 1;
149 }
150
151 fn finish(&self) -> Outcome<ScopedCountResult> {
152 Outcome::Some(ScopedCountResult {
153 target: self.target.clone(),
154 count: self.count,
155 })
156 }
157 }
158
159 ScopedCount {
160 target,
161 pending_scope: false,
162 inside_scope: false,
163 count: Count::default(),
164 }
165}
166
167pub fn focus_previous<T>() -> impl Operation<T>
171where
172 T: Send + 'static,
173{
174 struct FocusPrevious {
175 count: Count,
176 current: usize,
177 }
178
179 impl<T> Operation<T> for FocusPrevious {
180 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
181 if self.count.total == 0 {
182 return;
183 }
184
185 match self.count.focused {
186 None if self.current == self.count.total - 1 => state.focus(),
187 Some(0) if self.current == self.count.total - 1 => {
188 state.focus();
189 }
190 Some(0) if self.current == 0 => state.unfocus(),
191 Some(0) => {}
192 Some(focused) if focused == self.current => state.unfocus(),
193 Some(focused) if focused - 1 == self.current => state.focus(),
194 _ => {}
195 }
196
197 self.current += 1;
198 }
199
200 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
201 operate(self);
202 }
203 }
204
205 operation::then(count(), |count| FocusPrevious { count, current: 0 })
206}
207
208pub fn focus_next<T>() -> impl Operation<T>
212where
213 T: Send + 'static,
214{
215 struct FocusNext {
216 count: Count,
217 current: usize,
218 }
219
220 impl<T> Operation<T> for FocusNext {
221 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
222 match self.count.focused {
223 None if self.current == 0 => state.focus(),
224 Some(focused) if focused == self.count.total - 1 && self.current == 0 => {
225 state.focus();
226 }
227 Some(focused) if focused == self.current => state.unfocus(),
228 Some(focused) if focused + 1 == self.current => state.focus(),
229 _ => {}
230 }
231
232 self.current += 1;
233 }
234
235 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
236 operate(self);
237 }
238 }
239
240 operation::then(count(), |count| FocusNext { count, current: 0 })
241}
242
243pub fn focus_previous_within<T>(target: Id) -> impl Operation<T>
250where
251 T: Send + 'static,
252{
253 struct ScopedFocusPrevious {
254 target: Id,
255 pending_scope: bool,
256 inside_scope: bool,
257 count: Count,
258 current: usize,
259 }
260
261 impl<T> Operation<T> for ScopedFocusPrevious {
262 fn container(&mut self, id: Option<&Id>, _bounds: Rectangle) {
263 if id.is_some_and(|id| *id == self.target) {
264 self.pending_scope = true;
265 }
266 }
267
268 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
269 let was_inside = self.inside_scope;
270 if self.pending_scope {
271 self.inside_scope = true;
272 self.pending_scope = false;
273 }
274 operate(self);
275 self.inside_scope = was_inside;
276 }
277
278 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
279 if !self.inside_scope {
280 return;
281 }
282
283 if self.count.total == 0 {
284 return;
285 }
286
287 match self.count.focused {
288 None if self.current == self.count.total - 1 => state.focus(),
289 Some(0) if self.current == self.count.total - 1 => {
290 state.focus();
291 }
292 Some(0) if self.current == 0 => state.unfocus(),
293 Some(0) => {}
294 Some(focused) if focused == self.current => state.unfocus(),
295 Some(focused) if focused - 1 == self.current => state.focus(),
296 _ => {}
297 }
298
299 self.current += 1;
300 }
301 }
302
303 operation::then(scoped_count(target), |result| ScopedFocusPrevious {
304 target: result.target,
305 pending_scope: false,
306 inside_scope: false,
307 count: result.count,
308 current: 0,
309 })
310}
311
312pub fn focus_next_within<T>(target: Id) -> impl Operation<T>
319where
320 T: Send + 'static,
321{
322 struct ScopedFocusNext {
323 target: Id,
324 pending_scope: bool,
325 inside_scope: bool,
326 count: Count,
327 current: usize,
328 }
329
330 impl<T> Operation<T> for ScopedFocusNext {
331 fn container(&mut self, id: Option<&Id>, _bounds: Rectangle) {
332 if id.is_some_and(|id| *id == self.target) {
333 self.pending_scope = true;
334 }
335 }
336
337 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
338 let was_inside = self.inside_scope;
339 if self.pending_scope {
340 self.inside_scope = true;
341 self.pending_scope = false;
342 }
343 operate(self);
344 self.inside_scope = was_inside;
345 }
346
347 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
348 if !self.inside_scope {
349 return;
350 }
351
352 match self.count.focused {
353 None if self.current == 0 => state.focus(),
354 Some(focused) if focused == self.count.total - 1 && self.current == 0 => {
355 state.focus();
356 }
357 Some(focused) if focused == self.current => state.unfocus(),
358 Some(focused) if focused + 1 == self.current => state.focus(),
359 _ => {}
360 }
361
362 self.current += 1;
363 }
364 }
365
366 operation::then(scoped_count(target), |result| ScopedFocusNext {
367 target: result.target,
368 pending_scope: false,
369 inside_scope: false,
370 count: result.count,
371 current: 0,
372 })
373}
374
375pub fn find_focused() -> impl Operation<Id> {
378 struct FindFocused {
379 focused: Option<Id>,
380 }
381
382 impl Operation<Id> for FindFocused {
383 fn focusable(&mut self, id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
384 if state.is_focused() && id.is_some() {
385 self.focused = id.cloned();
386 }
387 }
388
389 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Id>)) {
390 operate(self);
391 }
392
393 fn finish(&self) -> Outcome<Id> {
394 if let Some(id) = &self.focused {
395 Outcome::Some(id.clone())
396 } else {
397 Outcome::None
398 }
399 }
400 }
401
402 FindFocused { focused: None }
403}
404
405pub fn is_focused(target: Id) -> impl Operation<bool> {
409 struct IsFocused {
410 target: Id,
411 is_focused: Option<bool>,
412 }
413
414 impl Operation<bool> for IsFocused {
415 fn focusable(&mut self, id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
416 if id.is_some_and(|id| *id == self.target) {
417 self.is_focused = Some(state.is_focused());
418 }
419 }
420
421 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<bool>)) {
422 if self.is_focused.is_some() {
423 return;
424 }
425
426 operate(self);
427 }
428
429 fn finish(&self) -> Outcome<bool> {
430 self.is_focused.map_or(Outcome::None, Outcome::Some)
431 }
432 }
433
434 IsFocused {
435 target,
436 is_focused: None,
437 }
438}
439
440#[derive(Debug, Clone, Copy)]
442struct ScrollableInfo {
443 bounds: Rectangle,
444 content_bounds: Rectangle,
445 translation: Vector,
446}
447
448impl ScrollableInfo {
449 fn can_scroll(&self, action: ScrollAction) -> bool {
451 let max_y = (self.content_bounds.height - self.bounds.height).max(0.0);
452 let max_x = (self.content_bounds.width - self.bounds.width).max(0.0);
453
454 match action {
455 ScrollAction::PageDown | ScrollAction::LineDown => self.translation.y < max_y - 0.5,
456 ScrollAction::PageUp | ScrollAction::LineUp => self.translation.y > 0.5,
457 ScrollAction::LineRight | ScrollAction::PageRight => self.translation.x < max_x - 0.5,
458 ScrollAction::LineLeft | ScrollAction::PageLeft => self.translation.x > 0.5,
459 ScrollAction::Home => self.translation.y > 0.5 || self.translation.x > 0.5,
460 ScrollAction::End => {
461 self.translation.y < max_y - 0.5 || self.translation.x < max_x - 0.5
462 }
463 ScrollAction::ShiftHome => self.translation.x > 0.5,
464 ScrollAction::ShiftEnd => self.translation.x < max_x - 0.5,
465 }
466 }
467}
468
469#[derive(Debug, Clone, Copy)]
471struct ScrollAdjustment {
472 scrollable_bounds: Rectangle,
473 offset: AbsoluteOffset<Option<f32>>,
474}
475
476const SCROLL_MARGIN: f32 = 12.0;
480
481fn compute_scroll_to(
491 sb: Rectangle,
492 content_bounds: Rectangle,
493 t: Vector,
494 target: Rectangle,
495) -> Option<AbsoluteOffset<Option<f32>>> {
496 let cx = target.x - sb.x;
501 let cy = target.y - sb.y;
502
503 let scrollbar_reserved = 12.0;
508
509 let has_h_scrollbar = content_bounds.width > sb.width;
510 let has_v_scrollbar = content_bounds.height > sb.height;
511
512 let visible_w = if has_v_scrollbar {
513 sb.width - scrollbar_reserved
514 } else {
515 sb.width
516 };
517 let visible_h = if has_h_scrollbar {
518 sb.height - scrollbar_reserved
519 } else {
520 sb.height
521 };
522
523 let mut offset_x = None;
524 let mut offset_y = None;
525
526 if target.width >= visible_w || cx < t.x {
529 offset_x = Some((cx - SCROLL_MARGIN).max(0.0));
530 } else if cx + target.width > t.x + visible_w {
531 offset_x = Some(cx + target.width - visible_w + SCROLL_MARGIN);
532 }
533
534 if target.height >= visible_h || cy < t.y {
535 offset_y = Some((cy - SCROLL_MARGIN).max(0.0));
536 } else if cy + target.height > t.y + visible_h {
537 offset_y = Some(cy + target.height - visible_h + SCROLL_MARGIN);
538 }
539
540 if offset_x.is_some() || offset_y.is_some() {
541 Some(AbsoluteOffset {
542 x: offset_x,
543 y: offset_y,
544 })
545 } else {
546 None
547 }
548}
549
550pub fn scroll_focused_into_view<T>() -> impl Operation<T>
558where
559 T: Send + 'static,
560{
561 struct FindFocusedScrollContext {
562 pending_scrollable: Option<ScrollableInfo>,
563 scrollable_stack: Vec<ScrollableInfo>,
564 focused_bounds: Option<Rectangle>,
565 focused_ancestors: Vec<ScrollableInfo>,
566 }
567
568 impl Operation<Vec<ScrollAdjustment>> for FindFocusedScrollContext {
569 fn scrollable(
570 &mut self,
571 _id: Option<&Id>,
572 bounds: Rectangle,
573 content_bounds: Rectangle,
574 translation: Vector,
575 _state: &mut dyn Scrollable,
576 ) {
577 self.pending_scrollable = Some(ScrollableInfo {
578 bounds,
579 content_bounds,
580 translation,
581 });
582 }
583
584 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Vec<ScrollAdjustment>>)) {
585 if let Some(info) = self.pending_scrollable.take() {
586 self.scrollable_stack.push(info);
587 }
588
589 let depth = self.scrollable_stack.len();
590 operate(self);
591 self.scrollable_stack.truncate(depth);
592 }
593
594 fn focusable(&mut self, _id: Option<&Id>, bounds: Rectangle, state: &mut dyn Focusable) {
595 if state.is_focused() {
596 self.focused_bounds = Some(bounds);
597 self.focused_ancestors = self.scrollable_stack.clone();
598 }
599 }
600
601 fn finish(&self) -> Outcome<Vec<ScrollAdjustment>> {
602 let Some(focused) = self.focused_bounds else {
603 return Outcome::None;
604 };
605
606 let mut adjustments = Vec::new();
607
608 let mut target_bounds = focused;
611
612 for ancestor in self.focused_ancestors.iter().rev() {
613 if let Some(offset) = compute_scroll_to(
614 ancestor.bounds,
615 ancestor.content_bounds,
616 ancestor.translation,
617 target_bounds,
618 ) {
619 adjustments.push(ScrollAdjustment {
620 scrollable_bounds: ancestor.bounds,
621 offset,
622 });
623 }
624
625 target_bounds = ancestor.bounds;
627 }
628
629 if adjustments.is_empty() {
630 Outcome::None
631 } else {
632 Outcome::Some(adjustments)
633 }
634 }
635 }
636
637 struct ApplyScrollAdjustments {
638 adjustments: Vec<ScrollAdjustment>,
639 applied: usize,
640 }
641
642 impl<T> Operation<T> for ApplyScrollAdjustments {
643 fn scrollable(
644 &mut self,
645 _id: Option<&Id>,
646 bounds: Rectangle,
647 _content_bounds: Rectangle,
648 _translation: Vector,
649 state: &mut dyn Scrollable,
650 ) {
651 if let Some(adj) = self
652 .adjustments
653 .iter()
654 .find(|a| a.scrollable_bounds == bounds)
655 {
656 state.scroll_to(adj.offset);
657 self.applied += 1;
658 }
659 }
660
661 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
662 if self.applied < self.adjustments.len() {
663 operate(self);
664 }
665 }
666 }
667
668 operation::then(
669 FindFocusedScrollContext {
670 pending_scrollable: None,
671 scrollable_stack: Vec::new(),
672 focused_bounds: None,
673 focused_ancestors: Vec::new(),
674 },
675 |adjustments| ApplyScrollAdjustments {
676 adjustments,
677 applied: 0,
678 },
679 )
680}
681
682#[derive(Debug, Clone, Copy)]
684pub enum ScrollAction {
685 PageUp,
687 PageDown,
689 LineUp,
691 LineDown,
693 LineLeft,
695 LineRight,
697 Home,
699 End,
701 PageLeft,
703 PageRight,
705 ShiftHome,
707 ShiftEnd,
709}
710
711struct ScrollTarget {
713 scrollable_bounds: Rectangle,
714 action: ScrollAction,
715}
716
717pub fn scroll_focused_ancestor<T>(action: ScrollAction) -> impl Operation<T>
724where
725 T: Send + 'static,
726{
727 struct FindTarget {
728 action: ScrollAction,
729 pending_scrollable: Option<ScrollableInfo>,
730 scrollable_stack: Vec<ScrollableInfo>,
731 focused_ancestors: Option<Vec<ScrollableInfo>>,
732 all_scrollables: Vec<ScrollableInfo>,
733 }
734
735 impl Operation<ScrollTarget> for FindTarget {
736 fn scrollable(
737 &mut self,
738 _id: Option<&Id>,
739 bounds: Rectangle,
740 content_bounds: Rectangle,
741 translation: Vector,
742 _state: &mut dyn Scrollable,
743 ) {
744 let info = ScrollableInfo {
745 bounds,
746 content_bounds,
747 translation,
748 };
749
750 self.pending_scrollable = Some(info);
751 self.all_scrollables.push(info);
752 }
753
754 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<ScrollTarget>)) {
755 if let Some(info) = self.pending_scrollable.take() {
756 self.scrollable_stack.push(info);
757 }
758
759 let depth = self.scrollable_stack.len();
760 operate(self);
761 self.scrollable_stack.truncate(depth);
762 }
763
764 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
765 if state.is_focused() && self.focused_ancestors.is_none() {
766 self.focused_ancestors = Some(self.scrollable_stack.clone());
767 }
768 }
769
770 fn finish(&self) -> Outcome<ScrollTarget> {
771 if let Some(ancestors) = &self.focused_ancestors {
773 for ancestor in ancestors.iter().rev() {
774 if ancestor.can_scroll(self.action) {
775 return Outcome::Some(ScrollTarget {
776 scrollable_bounds: ancestor.bounds,
777 action: self.action,
778 });
779 }
780 }
781 }
782
783 let reverse = matches!(
788 self.action,
789 ScrollAction::PageUp
790 | ScrollAction::LineUp
791 | ScrollAction::LineLeft
792 | ScrollAction::Home
793 | ScrollAction::PageLeft
794 | ScrollAction::ShiftHome
795 );
796
797 let find = |scrollables: &[ScrollableInfo]| {
798 scrollables.iter().position(|s| s.can_scroll(self.action))
799 };
800
801 let index = if reverse {
802 self.all_scrollables
804 .iter()
805 .rposition(|s| s.can_scroll(self.action))
806 } else {
807 find(&self.all_scrollables)
808 };
809
810 if let Some(i) = index {
811 return Outcome::Some(ScrollTarget {
812 scrollable_bounds: self.all_scrollables[i].bounds,
813 action: self.action,
814 });
815 }
816
817 Outcome::None
818 }
819 }
820
821 struct ApplyScroll {
822 target: ScrollTarget,
823 }
824
825 impl<T> Operation<T> for ApplyScroll {
826 fn scrollable(
827 &mut self,
828 _id: Option<&Id>,
829 bounds: Rectangle,
830 content_bounds: Rectangle,
831 _translation: Vector,
832 state: &mut dyn Scrollable,
833 ) {
834 if bounds != self.target.scrollable_bounds {
835 return;
836 }
837
838 let line_height = 16.0;
841
842 match self.target.action {
843 ScrollAction::PageDown => {
844 state.scroll_by(
845 AbsoluteOffset {
846 x: 0.0,
847 y: bounds.height,
848 },
849 bounds,
850 content_bounds,
851 );
852 }
853 ScrollAction::PageUp => {
854 state.scroll_by(
855 AbsoluteOffset {
856 x: 0.0,
857 y: -bounds.height,
858 },
859 bounds,
860 content_bounds,
861 );
862 }
863 ScrollAction::LineDown => {
864 state.scroll_by(
865 AbsoluteOffset {
866 x: 0.0,
867 y: line_height,
868 },
869 bounds,
870 content_bounds,
871 );
872 }
873 ScrollAction::LineUp => {
874 state.scroll_by(
875 AbsoluteOffset {
876 x: 0.0,
877 y: -line_height,
878 },
879 bounds,
880 content_bounds,
881 );
882 }
883 ScrollAction::LineRight => {
884 state.scroll_by(
885 AbsoluteOffset {
886 x: line_height,
887 y: 0.0,
888 },
889 bounds,
890 content_bounds,
891 );
892 }
893 ScrollAction::LineLeft => {
894 state.scroll_by(
895 AbsoluteOffset {
896 x: -line_height,
897 y: 0.0,
898 },
899 bounds,
900 content_bounds,
901 );
902 }
903 ScrollAction::Home => {
904 let overflows_x = content_bounds.width > bounds.width;
905 let overflows_y = content_bounds.height > bounds.height;
906
907 state.snap_to(RelativeOffset {
908 x: if overflows_x && !overflows_y {
909 Some(0.0)
910 } else {
911 None
912 },
913 y: if overflows_y || !overflows_x {
914 Some(0.0)
915 } else {
916 None
917 },
918 });
919 }
920 ScrollAction::End => {
921 let overflows_x = content_bounds.width > bounds.width;
922 let overflows_y = content_bounds.height > bounds.height;
923
924 state.snap_to(RelativeOffset {
925 x: if overflows_x && !overflows_y {
926 Some(1.0)
927 } else {
928 None
929 },
930 y: if overflows_y || !overflows_x {
931 Some(1.0)
932 } else {
933 None
934 },
935 });
936 }
937 ScrollAction::PageRight => {
938 state.scroll_by(
939 AbsoluteOffset {
940 x: bounds.width,
941 y: 0.0,
942 },
943 bounds,
944 content_bounds,
945 );
946 }
947 ScrollAction::PageLeft => {
948 state.scroll_by(
949 AbsoluteOffset {
950 x: -bounds.width,
951 y: 0.0,
952 },
953 bounds,
954 content_bounds,
955 );
956 }
957 ScrollAction::ShiftHome => {
958 state.snap_to(RelativeOffset {
959 x: Some(0.0),
960 y: None,
961 });
962 }
963 ScrollAction::ShiftEnd => {
964 state.snap_to(RelativeOffset {
965 x: Some(1.0),
966 y: None,
967 });
968 }
969 }
970 }
971
972 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
973 operate(self);
974 }
975 }
976
977 operation::then(
978 FindTarget {
979 action,
980 pending_scrollable: None,
981 scrollable_stack: Vec::new(),
982 focused_ancestors: None,
983 all_scrollables: Vec::new(),
984 },
985 |target| ApplyScroll { target },
986 )
987}
988
989#[derive(Debug, Clone)]
991pub struct MnemonicTarget {
992 pub bounds: Rectangle,
994 pub id: Option<Id>,
996}
997
998pub fn find_mnemonic(key: char) -> impl Operation<MnemonicTarget> {
1004 struct FindMnemonic {
1005 key: char,
1006 found: Option<MnemonicTarget>,
1007 }
1008
1009 impl Operation<MnemonicTarget> for FindMnemonic {
1010 fn accessible(&mut self, id: Option<&Id>, bounds: Rectangle, accessible: &Accessible<'_>) {
1011 if self.found.is_some() {
1012 return;
1013 }
1014
1015 if let Some(mnemonic) = accessible.mnemonic
1016 && mnemonic.eq_ignore_ascii_case(&self.key)
1017 && !accessible.disabled
1018 {
1019 self.found = Some(MnemonicTarget {
1020 bounds,
1021 id: id.cloned(),
1022 });
1023 }
1024 }
1025
1026 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<MnemonicTarget>)) {
1027 if self.found.is_none() {
1028 operate(self);
1029 }
1030 }
1031
1032 fn finish(&self) -> Outcome<MnemonicTarget> {
1033 match &self.found {
1034 Some(target) => Outcome::Some(target.clone()),
1035 None => Outcome::None,
1036 }
1037 }
1038 }
1039
1040 FindMnemonic { key, found: None }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046 use crate::{Point, Size};
1047
1048 fn rect(x: f32, y: f32, w: f32, h: f32) -> Rectangle {
1049 Rectangle::new(Point::new(x, y), Size::new(w, h))
1050 }
1051
1052 fn vec2(x: f32, y: f32) -> Vector {
1053 Vector::new(x, y)
1054 }
1055
1056 #[test]
1061 fn both_scrollbars_reduce_visible_area() {
1062 let sb = rect(0.0, 0.0, 400.0, 300.0);
1066 let content = rect(0.0, 0.0, 600.0, 800.0);
1067
1068 let target = rect(339.0, 50.0, 50.0, 40.0);
1070 let result = compute_scroll_to(sb, content, vec2(0.0, 0.0), target);
1071
1072 assert!(
1073 result.is_some(),
1074 "should scroll when target is behind scrollbar"
1075 );
1076 assert!(result.unwrap().x.expect("horizontal scroll") > 0.0);
1077 }
1078
1079 #[test]
1080 fn sequential_tab_forward_and_backward() {
1081 let sb = rect(0.0, 0.0, 400.0, 200.0);
1088 let content = rect(0.0, 0.0, 600.0, 800.0);
1089
1090 let btn1 = rect(50.0, 10.0, 100.0, 40.0);
1091 let btn2 = rect(50.0, 300.0, 100.0, 40.0);
1092 let btn3 = rect(50.0, 600.0, 100.0, 40.0);
1093
1094 let r1 = compute_scroll_to(sb, content, vec2(0.0, 0.0), btn1);
1096 assert!(r1.is_none(), "btn1 should be visible at scroll=0");
1097
1098 let r2 = compute_scroll_to(sb, content, vec2(0.0, 0.0), btn2).unwrap();
1100 let scroll_y = r2.y.expect("should scroll to btn2");
1101 assert!(
1103 (scroll_y - 164.0).abs() < 0.1,
1104 "btn2: expected ~164, got {scroll_y}"
1105 );
1106
1107 let r3 = compute_scroll_to(sb, content, vec2(0.0, scroll_y), btn3).unwrap();
1109 let scroll_y = r3.y.expect("should scroll to btn3");
1110 assert!(
1112 (scroll_y - 464.0).abs() < 0.1,
1113 "btn3: expected ~464, got {scroll_y}"
1114 );
1115
1116 let r4 = compute_scroll_to(sb, content, vec2(0.0, scroll_y), btn2).unwrap();
1118 let scroll_y = r4.y.expect("should scroll back to btn2");
1119 assert!(
1121 (scroll_y - 288.0).abs() < 0.1,
1122 "back to btn2: expected ~288, got {scroll_y}"
1123 );
1124
1125 let r5 = compute_scroll_to(sb, content, vec2(0.0, scroll_y), btn1).unwrap();
1127 let scroll_y = r5.y.expect("should scroll back to btn1");
1128 assert!(
1130 (scroll_y - 0.0).abs() < 0.1,
1131 "back to btn1: expected ~0, got {scroll_y}"
1132 );
1133 }
1134}