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