1use std::any::Any;
2use std::cell::RefCell;
3use std::panic::Location;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use rustc_hash::FxHashMap;
8
9use crate::scope::Scope;
10use crate::{Rect, Scene, View, semantics::Role};
11
12thread_local! {
13 pub static COMPOSER: RefCell<Composer> = RefCell::new(Composer::default());
14 static ROOT_SCOPE: RefCell<Option<Scope>> = const { RefCell::new(None) };
15
16 static FOCUS_REQUESTS: RefCell<std::collections::VecDeque<u64>> =
20 const { RefCell::new(std::collections::VecDeque::new()) };
21}
22
23pub const CLEAR_FOCUS_MARKER: u64 = u64::MAX;
25
26static COMPONENT_ID: AtomicU64 = AtomicU64::new(1);
29
30pub fn unique_component_id() -> u64 {
32 COMPONENT_ID.fetch_add(1, Ordering::Relaxed)
33}
34
35pub fn take_focus_request() -> Option<u64> {
36 FOCUS_REQUESTS.with(|r| r.borrow_mut().pop_front())
37}
38
39pub fn drain_focus_requests() -> Vec<u64> {
41 FOCUS_REQUESTS.with(|r| r.borrow_mut().drain(..).collect())
42}
43
44#[derive(Clone)]
50pub struct FocusRequester {
51 pub target: Rc<RefCell<Option<u64>>>,
53}
54
55impl FocusRequester {
56 pub fn new() -> Self {
57 Self {
58 target: Rc::new(RefCell::new(None)),
59 }
60 }
61
62 pub fn request_focus(&self) {
67 if let Some(id) = *self.target.borrow() {
68 FOCUS_REQUESTS.with(|r| r.borrow_mut().push_back(id));
69 }
70 }
71
72 pub fn free_focus(&self) {
76 FOCUS_REQUESTS.with(|r| r.borrow_mut().push_back(CLEAR_FOCUS_MARKER));
77 }
78
79 pub fn capture_focus(&self) {
84 self.request_focus();
85 }
86}
87
88impl Default for FocusRequester {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum FocusDirection {
97 Next,
98 Previous,
99 Left,
100 Right,
101 Up,
102 Down,
103}
104
105#[derive(Clone)]
110pub struct FocusManager {
111 pub chain: Vec<u64>,
113 pub focused: Option<u64>,
115 pub hit_regions: Vec<HitRegion>,
117}
118
119impl FocusManager {
120 pub fn new(chain: Vec<u64>, focused: Option<u64>) -> Self {
121 Self {
122 chain,
123 focused,
124 hit_regions: Vec::new(),
125 }
126 }
127
128 pub fn move_focus(&mut self, dir: FocusDirection) -> Option<u64> {
131 match dir {
132 FocusDirection::Next | FocusDirection::Previous => {
133 self.move_tab(dir == FocusDirection::Previous)
134 }
135 _ => None, }
137 }
138
139 pub fn clear_focus(&self, _force: bool) {
144 FOCUS_REQUESTS.with(|r| r.borrow_mut().push_back(CLEAR_FOCUS_MARKER));
145 }
146
147 pub fn move_focus_spatial(
150 &mut self,
151 dir: FocusDirection,
152 hit_regions: &[HitRegion],
153 ) -> Option<u64> {
154 let next = spatial_focus_next(&self.chain, hit_regions, self.focused, dir)?;
155 self.focused = Some(next);
156 Some(next)
157 }
158
159 pub fn move_tab(&mut self, reverse: bool) -> Option<u64> {
163 if self.chain.is_empty() {
164 return None;
165 }
166 let next = if let Some(sub_chain) =
167 focus_group_chain(&self.chain, &self.hit_regions, self.focused)
168 {
169 if sub_chain.is_empty() {
170 return None;
171 }
172 if let Some(cur) = self.focused {
173 if let Some(idx) = sub_chain.iter().position(|&id| id == cur) {
174 if reverse {
175 if idx == 0 {
176 sub_chain[sub_chain.len() - 1]
177 } else {
178 sub_chain[idx - 1]
179 }
180 } else {
181 sub_chain[(idx + 1) % sub_chain.len()]
182 }
183 } else {
184 sub_chain[0]
185 }
186 } else if reverse {
187 sub_chain[sub_chain.len() - 1]
188 } else {
189 sub_chain[0]
190 }
191 } else {
192 if let Some(cur) = self.focused {
193 if let Some(idx) = self.chain.iter().position(|&id| id == cur) {
194 if reverse {
195 if idx == 0 {
196 self.chain[self.chain.len() - 1]
197 } else {
198 self.chain[idx - 1]
199 }
200 } else {
201 self.chain[(idx + 1) % self.chain.len()]
202 }
203 } else {
204 self.chain[0]
205 }
206 } else if reverse {
207 self.chain[self.chain.len() - 1]
208 } else {
209 self.chain[0]
210 }
211 };
212 self.focused = Some(next);
213 Some(next)
214 }
215
216 pub fn set_requester_target(requester: &FocusRequester, id: u64) {
218 *requester.target.borrow_mut() = Some(id);
219 }
220}
221
222pub fn focus_group_chain(
226 chain: &[u64],
227 hit_regions: &[HitRegion],
228 current: Option<u64>,
229) -> Option<Vec<u64>> {
230 let cur = current?;
231 let group_id = hit_regions.iter().find(|h| h.id == cur)?.focus_group_id?;
232 Some(
233 chain
234 .iter()
235 .copied()
236 .filter(|&id| {
237 id == group_id
238 || hit_regions
239 .iter()
240 .any(|h| h.id == id && h.focus_group_id == Some(group_id))
241 })
242 .collect(),
243 )
244}
245
246pub fn spatial_focus_next(
251 chain: &[u64],
252 hit_regions: &[HitRegion],
253 current: Option<u64>,
254 dir: FocusDirection,
255) -> Option<u64> {
256 if chain.is_empty() {
257 return None;
258 }
259
260 let current_rect =
261 current.and_then(|id| hit_regions.iter().find(|h| h.id == id).map(|h| h.rect));
262
263 match dir {
265 FocusDirection::Next | FocusDirection::Previous => {
266 let mut fm = FocusManager {
267 chain: chain.to_vec(),
268 focused: current,
269 hit_regions: hit_regions.to_vec(),
270 };
271 return fm.move_tab(dir == FocusDirection::Previous);
272 }
273 _ => {}
274 }
275
276 let (cx, cy) = match current_rect {
277 Some(r) => (r.x + r.w / 2.0, r.y + r.h / 2.0),
278 None => {
279 return None;
281 }
282 };
283
284 let mut best: Option<(u64, f32)> = None;
285
286 let scoped: Vec<u64>;
288 let chain: &[u64] = match focus_group_chain(chain, hit_regions, current) {
289 Some(sub) => {
290 scoped = sub;
291 &scoped
292 }
293 None => chain,
294 };
295
296 for &id in chain {
297 if Some(id) == current {
298 continue;
299 }
300 let Some(hr) = hit_regions.iter().find(|h| h.id == id) else {
301 continue;
302 };
303 let r = hr.rect;
304 let other_cx = r.x + r.w / 2.0;
305 let other_cy = r.y + r.h / 2.0;
306 let dx = other_cx - cx;
307 let dy = other_cy - cy;
308
309 let in_direction = match dir {
310 FocusDirection::Left => dx < 0.0 && dy.abs() <= r.h.max(1.0),
311 FocusDirection::Right => dx > 0.0 && dy.abs() <= r.h.max(1.0),
312 FocusDirection::Up => dy < 0.0 && dx.abs() <= r.w.max(1.0),
313 FocusDirection::Down => dy > 0.0 && dx.abs() <= r.w.max(1.0),
314 _ => false,
315 };
316
317 if !in_direction {
318 continue;
319 }
320
321 let dist = dx * dx + dy * dy;
322 let weight = dist / (r.w * r.h + 1.0).max(1.0);
323
324 match best {
325 Some((_, best_weight)) if weight >= best_weight => {}
326 _ => best = Some((id, weight)),
327 }
328 }
329
330 best.map(|(id, _)| id)
331}
332
333#[derive(Default)]
334pub struct Composer {
335 pub slots: Vec<Box<dyn Any>>,
336 pub slot_callers: Vec<&'static Location<'static>>,
339 pub cursor: usize,
340 pub keyed_slots: FxHashMap<String, Box<dyn Any>>,
341 pub scope_caches: FxHashMap<String, crate::scope_cache::ScopeCache>,
344}
345
346pub struct ComposeGuard {
347 scope: Scope,
348}
349
350impl ComposeGuard {
351 pub fn begin() -> Self {
352 COMPOSER.with(|c| c.borrow_mut().cursor = 0);
353
354 let scope = ROOT_SCOPE.with(|rs| {
355 if let Some(existing) = rs.borrow().clone() {
356 existing
357 } else {
358 let s = Scope::new();
359 *rs.borrow_mut() = Some(s.clone());
360 s
361 }
362 });
363
364 ComposeGuard { scope }
365 }
366
367 pub fn scope(&self) -> &Scope {
368 &self.scope
369 }
370}
371
372impl Drop for ComposeGuard {
373 fn drop(&mut self) {
374 }
378}
379
380#[track_caller]
383pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
384 let caller = Location::caller();
387 COMPOSER.with(|c| {
388 let mut c = c.borrow_mut();
389 let cursor = c.cursor;
390 c.cursor += 1;
391
392 if cursor >= c.slots.len() {
393 let rc: Rc<T> = Rc::new(init());
394 c.slots.push(Box::new(rc.clone()));
395 c.slot_callers.push(caller);
396 return rc;
397 }
398
399 let stored_caller = c.slot_callers.get(cursor).copied();
400 if stored_caller != Some(caller) {
401 let rc: Rc<T> = Rc::new(init());
402 c.slots[cursor] = Box::new(rc.clone());
403 if cursor < c.slot_callers.len() {
404 c.slot_callers[cursor] = caller;
405 } else {
406 c.slot_callers.push(caller);
407 }
408 return rc;
409 }
410
411 if let Some(rc) = c.slots[cursor].downcast_ref::<Rc<T>>() {
412 rc.clone()
413 } else {
414 log::warn!(
415 "remember: slot {} type changed {}. \
416 Use remember_with_key(key, || ...) for conditional branches.",
417 cursor,
418 std::any::type_name::<T>(),
419 );
420 let rc: Rc<T> = Rc::new(init());
421 c.slots[cursor] = Box::new(rc.clone());
422 rc
423 }
424 })
425}
426
427pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
429 COMPOSER.with(|c| {
430 let mut c = c.borrow_mut();
431 let key = key.into();
432
433 if let Some(existing) = c.keyed_slots.get(&key) {
434 if let Some(rc) = existing.downcast_ref::<Rc<T>>() {
435 return rc.clone();
436 } else {
437 log::warn!(
438 "remember_with_key: key '{}' reused with a different type; replacing.",
439 key
440 );
441 }
442 }
443
444 if cfg!(debug_assertions) && c.keyed_slots.len() > 10_000 {
445 log::warn!(
446 "remember_with_key: more than 10k keys stored; \
447 are you generating unbounded dynamic keys (e.g., using timestamps)?"
448 );
449 }
450
451 let rc: Rc<T> = Rc::new(init());
452 c.keyed_slots.insert(key, Box::new(rc.clone()));
453 rc
454 })
455}
456
457#[track_caller]
460pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
461 remember(|| RefCell::new(init()))
462}
463
464pub fn remember_state_with_key<T: 'static>(
466 key: impl Into<String>,
467 init: impl FnOnce() -> T,
468) -> Rc<RefCell<T>> {
469 remember_with_key(key, || RefCell::new(init()))
470}
471
472#[derive(Clone)]
474pub struct Frame {
475 pub scene: Scene,
476 pub hit_regions: Vec<HitRegion>,
477 pub semantics_nodes: Vec<SemNode>,
478 pub focus_chain: Vec<u64>,
479}
480
481#[derive(Clone, Default)]
484pub struct HitRegion {
485 pub id: u64,
486 pub rect: Rect,
487 pub depth: u32,
490 pub parent: Option<u64>,
491 pub on_click: Option<Rc<dyn Fn()>>,
492 pub on_double_click: Option<Rc<dyn Fn()>>,
493 pub on_long_click: Option<Rc<dyn Fn()>>,
494 pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
495 pub focusable: bool,
496 pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
497 pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
498 pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
499 pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
500 pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
501 pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
502 pub z_index: f32,
503 pub disabled: bool,
504 pub on_text_change: Option<Rc<dyn Fn(String)>>,
505 pub on_text_submit: Option<Rc<dyn Fn(String)>>,
506 pub tf_state_key: Option<u64>,
509
510 pub tf_multiline: bool,
512
513 pub tf_content_origin: Option<(f32, f32)>,
518
519 pub tf_enabled: bool,
521
522 pub tf_read_only: bool,
524
525 pub tf_value: String,
527
528 pub tf_font_size: crate::units::Sp,
531
532 pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
534 pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
535 pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
536 pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
537 pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
538 pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
539 pub drag_preview: Option<crate::dnd::DragPreview>,
541
542 pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
543
544 pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
547 pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
549
550 pub cursor: Option<crate::CursorIcon>,
552
553 pub focus_group_id: Option<u64>,
558
559 pub keyboard_type: crate::text::KeyboardType,
562 pub capitalization: crate::text::KeyboardCapitalization,
563 pub ime_action: crate::text::ImeAction,
564 pub auto_correct: Option<bool>,
567
568 pub interaction_source: Option<crate::modifier::InteractionSource>,
573}
574
575impl HitRegion {
576 pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
580 Self {
581 id,
582 rect,
583 z_index: m.z_index,
584 on_click: m.on_click.clone(),
585 on_double_click: m.on_double_click.clone(),
586 on_long_click: m.on_long_click.clone(),
587 on_pointer_down: m.on_pointer_down.clone(),
588 on_pointer_move: m.on_pointer_move.clone(),
589 on_pointer_up: m.on_pointer_up.clone(),
590 on_pointer_cancel: m.on_pointer_cancel.clone(),
591 on_pointer_enter: m.on_pointer_enter.clone(),
592 on_pointer_leave: m.on_pointer_leave.clone(),
593 on_action: m.on_action.clone(),
594 on_key_event: m.on_key_event.clone(),
595 on_preview_key_event: m.on_preview_key_event.clone(),
596 cursor: m.cursor,
597 on_drag_start: m.on_drag_start.clone(),
598 on_drag_end: m.on_drag_end.clone(),
599 on_drag_enter: m.on_drag_enter.clone(),
600 on_drag_over: m.on_drag_over.clone(),
601 on_drag_leave: m.on_drag_leave.clone(),
602 on_scroll: m.on_scroll.clone(),
603 on_drop: m.on_drop.clone(),
604 drag_preview: m.drag_preview.clone(),
605 disabled: m.disabled,
606 tf_enabled: true,
607 tf_read_only: false,
608 ..Default::default()
609 }
610 }
611}
612
613#[derive(Clone)]
621pub struct SemNode {
622 pub id: u64,
624
625 pub parent: Option<u64>,
627
628 pub role: Role,
629 pub label: Option<String>,
630 pub rect: Rect,
631 pub focused: bool,
632 pub enabled: bool,
633 pub selectable_group: bool,
635 pub checked: Option<bool>,
636 pub selected: Option<bool>,
637 pub value: Option<String>,
638}
639
640impl Default for SemNode {
641 fn default() -> Self {
642 Self {
643 id: 0,
644 parent: None,
645 role: Role::default(),
646 label: None,
647 rect: Rect::default(),
648 focused: false,
649 enabled: true,
650 selectable_group: false,
651 checked: None,
652 selected: None,
653 value: None,
654 }
655 }
656}
657
658pub struct Scheduler {
659 next_id: u64,
660 scope_key_to_id: FxHashMap<String, u32>,
663 next_scope_id: u32,
664 current_scope: Vec<String>,
669 scope_local_counters: FxHashMap<String, u32>,
671 pub focused: Option<u64>,
672 pub size: (u32, u32),
673}
674
675impl Default for Scheduler {
676 fn default() -> Self {
677 Self::new()
678 }
679}
680
681impl Scheduler {
682 pub fn new() -> Self {
683 Self {
684 next_id: 1,
685 scope_key_to_id: FxHashMap::default(),
686 next_scope_id: 1,
687 current_scope: Vec::new(),
688 scope_local_counters: FxHashMap::default(),
689 focused: None,
690 size: (1280, 800),
691 }
692 }
693
694 pub fn enter_scope(&mut self, key: &str) {
699 if !self.current_scope.iter().any(|k| k == key) {
700 self.scope_local_counters.insert(key.to_string(), 0);
701 }
702 self.current_scope.push(key.to_string());
703 self.get_or_create_scope_id(key);
704 }
705
706 pub fn exit_scope(&mut self) {
709 self.current_scope.pop();
710 }
711
712 pub fn scope_guard<'a>(&'a mut self, key: &str) -> SchedulerScopeGuard<'a> {
716 self.enter_scope(key);
717 SchedulerScopeGuard { sched: self }
718 }
719
720 pub fn scope_guard_raw(&mut self, key: &str) -> SchedulerScopeGuardRaw {
724 let ptr = self as *mut Scheduler;
725 unsafe { SchedulerScopeGuardRaw::enter(ptr, key) }
726 }
727
728 fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
729 if let Some(&id) = self.scope_key_to_id.get(key) {
730 id
731 } else {
732 let id = self.next_scope_id;
733 self.next_scope_id += 1;
734 self.scope_key_to_id.insert(key.to_string(), id);
735 id
736 }
737 }
738
739 pub fn id(&mut self) -> u64 {
740 if let Some(key) = self.current_scope.last().cloned() {
741 let scope_id = self.scope_key_to_id.get(&key).copied().unwrap_or(0);
742 let local = self.scope_local_counters.get_mut(&key).unwrap();
743 let id = *local;
744 *local += 1;
745 (scope_id as u64) << 32 | id as u64
746 } else {
747 let id = self.next_id;
749 self.next_id += 1;
750 id
751 }
752 }
753
754 pub fn id_count(&self) -> u64 {
755 self.next_id - 1
756 }
757
758 pub fn snapshot_id(&self) -> u64 {
761 self.next_id
762 }
763
764 pub fn advance_id(&mut self, count: u32) {
767 self.next_id += count as u64;
768 }
769
770 pub fn ids_used_since(&self, prev_id: u64) -> u32 {
773 (self.next_id - prev_id) as u32
774 }
775}
776
777pub struct SchedulerScopeGuard<'a> {
779 sched: &'a mut Scheduler,
780}
781
782impl Drop for SchedulerScopeGuard<'_> {
783 fn drop(&mut self) {
784 self.sched.exit_scope();
785 }
786}
787
788pub struct SchedulerScopeGuardRaw {
795 sched: *mut Scheduler,
796}
797
798impl SchedulerScopeGuardRaw {
799 pub unsafe fn enter(sched: *mut Scheduler, key: &str) -> Self {
804 unsafe {
805 (*sched).enter_scope(key);
806 }
807 Self { sched }
808 }
809}
810
811impl Drop for SchedulerScopeGuardRaw {
812 fn drop(&mut self) {
813 unsafe {
814 (*self.sched).exit_scope();
815 }
816 }
817}
818
819impl Scheduler {
820 pub fn repose<F>(
821 &mut self,
822 mut build_root: F,
823 layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
824 ) -> Frame
825 where
826 F: FnMut(&mut Scheduler) -> View,
827 {
828 let guard = ComposeGuard::begin();
829 let root = guard.scope.run(|| build_root(self));
830 let (scene, hits, sem) = layout_paint(&root, self.size);
831
832 let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
833
834 Frame {
835 scene,
836 hit_regions: hits,
837 semantics_nodes: sem,
838 focus_chain,
839 }
840 }
841}
842
843#[cfg(test)]
845pub fn clear_composer() {
846 COMPOSER.with(|c| {
847 let mut c = c.borrow_mut();
848 c.slots.clear();
849 c.slot_callers.clear();
850 c.keyed_slots.clear();
851 c.scope_caches.clear();
852 c.cursor = 0;
853 });
854 ROOT_SCOPE.with(|rs| {
855 *rs.borrow_mut() = None;
856 });
857}
858
859#[cfg(test)]
860mod focus_trap_tests {
861 use super::*;
862
863 fn region(id: u64, x: f32, group: Option<u64>) -> HitRegion {
864 HitRegion {
865 id,
866 rect: Rect {
867 x,
868 y: 0.0,
869 w: 10.0,
870 h: 10.0,
871 },
872 focus_group_id: group,
873 ..Default::default()
874 }
875 }
876
877 #[test]
878 fn arrows_stay_inside_group() {
879 let chain = vec![1, 2, 3, 4];
882 let regions = vec![
883 region(1, 0.0, None),
884 region(2, 20.0, Some(9)),
885 region(3, 40.0, Some(9)),
886 region(4, 60.0, None),
887 ];
888 assert_eq!(
889 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Left),
890 None,
891 "outsider 1 is left of 2 but outside the group: trapped"
892 );
893 assert_eq!(
894 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Right),
895 Some(3)
896 );
897 assert_eq!(
898 spatial_focus_next(&chain, ®ions, Some(3), FocusDirection::Left),
899 Some(2)
900 );
901 assert_eq!(
902 spatial_focus_next(&chain, ®ions, Some(1), FocusDirection::Right),
903 Some(2),
904 "ungrouped focus still sees the full chain"
905 );
906 }
907
908 #[test]
909 fn tab_cycles_inside_group() {
910 let chain = vec![1, 2, 3, 4];
911 let regions = vec![
912 region(1, 0.0, None),
913 region(2, 20.0, Some(9)),
914 region(3, 40.0, Some(9)),
915 region(4, 60.0, None),
916 ];
917 let mut fm = FocusManager::new(chain, Some(2));
918 fm.hit_regions = regions;
919 assert_eq!(fm.move_tab(false), Some(3));
920 assert_eq!(fm.move_tab(false), Some(2));
921 assert_eq!(fm.move_tab(true), Some(3));
922 }
923
924 #[test]
925 fn tab_from_outside_can_enter_group() {
926 let chain = vec![1, 2, 3, 4];
927 let regions = vec![
928 region(1, 0.0, None),
929 region(2, 20.0, Some(9)),
930 region(3, 40.0, Some(9)),
931 region(4, 60.0, None),
932 ];
933 let mut fm = FocusManager::new(chain, Some(1));
934 fm.hit_regions = regions;
935 assert_eq!(fm.move_tab(false), Some(2));
936 let mut fm = FocusManager::new(vec![1, 2, 3, 4], Some(1));
937 fm.hit_regions = vec![
938 region(1, 0.0, None),
939 region(2, 20.0, Some(9)),
940 region(3, 40.0, Some(9)),
941 region(4, 60.0, None),
942 ];
943 assert_eq!(fm.move_tab(true), Some(4));
944 }
945
946 #[test]
947 fn empty_group_never_moves() {
948 let chain = vec![1, 4];
949 let regions = vec![region(1, 0.0, None), region(4, 60.0, None)];
950 let mut fm = FocusManager::new(chain, Some(1));
951 fm.hit_regions = regions.clone();
952 assert_eq!(fm.move_tab(false), Some(4));
953 let chain = vec![1, 2, 4];
954 let regions = vec![
955 region(1, 0.0, None),
956 region(2, 20.0, Some(77)),
957 region(4, 60.0, None),
958 ];
959 let mut fm = FocusManager::new(chain, Some(2));
960 fm.hit_regions = regions;
961 assert_eq!(fm.move_tab(false), Some(2));
962 assert_eq!(
963 spatial_focus_next(&fm.chain, &fm.hit_regions, Some(2), FocusDirection::Right),
964 None
965 );
966 }
967}