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::{CursorIcon, 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 pub held_keys: Vec<String>,
681 pub window_focused: bool,
684 pub mouse_primary: bool,
688 pub mouse_secondary: bool,
689 pub mouse_middle: bool,
690 pub cursor_override: Option<CursorIcon>,
698}
699
700impl Default for Scheduler {
701 fn default() -> Self {
702 Self::new()
703 }
704}
705
706impl Scheduler {
707 pub fn new() -> Self {
708 Self {
709 next_id: 1,
710 scope_key_to_id: FxHashMap::default(),
711 next_scope_id: 1,
712 current_scope: Vec::new(),
713 scope_local_counters: FxHashMap::default(),
714 focused: None,
715 size: (1280, 800),
716 held_keys: Vec::new(),
717 window_focused: true,
718 mouse_primary: false,
719 mouse_secondary: false,
720 mouse_middle: false,
721 cursor_override: None,
722 }
723 }
724
725 pub fn enter_scope(&mut self, key: &str) {
730 if !self.current_scope.iter().any(|k| k == key) {
731 self.scope_local_counters.insert(key.to_string(), 0);
732 }
733 self.current_scope.push(key.to_string());
734 self.get_or_create_scope_id(key);
735 }
736
737 pub fn exit_scope(&mut self) {
740 self.current_scope.pop();
741 }
742
743 pub fn scope_guard<'a>(&'a mut self, key: &str) -> SchedulerScopeGuard<'a> {
747 self.enter_scope(key);
748 SchedulerScopeGuard { sched: self }
749 }
750
751 pub fn scope_guard_raw(&mut self, key: &str) -> SchedulerScopeGuardRaw {
755 let ptr = self as *mut Scheduler;
756 unsafe { SchedulerScopeGuardRaw::enter(ptr, key) }
757 }
758
759 fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
760 if let Some(&id) = self.scope_key_to_id.get(key) {
761 id
762 } else {
763 let id = self.next_scope_id;
764 self.next_scope_id += 1;
765 self.scope_key_to_id.insert(key.to_string(), id);
766 id
767 }
768 }
769
770 pub fn id(&mut self) -> u64 {
771 if let Some(key) = self.current_scope.last().cloned() {
772 let scope_id = self.scope_key_to_id.get(&key).copied().unwrap_or(0);
773 let local = self.scope_local_counters.get_mut(&key).unwrap();
774 let id = *local;
775 *local += 1;
776 (scope_id as u64) << 32 | id as u64
777 } else {
778 let id = self.next_id;
780 self.next_id += 1;
781 id
782 }
783 }
784
785 pub fn id_count(&self) -> u64 {
786 self.next_id - 1
787 }
788
789 pub fn snapshot_id(&self) -> u64 {
792 self.next_id
793 }
794
795 pub fn advance_id(&mut self, count: u32) {
798 self.next_id += count as u64;
799 }
800
801 pub fn ids_used_since(&self, prev_id: u64) -> u32 {
804 (self.next_id - prev_id) as u32
805 }
806}
807
808pub struct SchedulerScopeGuard<'a> {
810 sched: &'a mut Scheduler,
811}
812
813impl Drop for SchedulerScopeGuard<'_> {
814 fn drop(&mut self) {
815 self.sched.exit_scope();
816 }
817}
818
819pub struct SchedulerScopeGuardRaw {
826 sched: *mut Scheduler,
827}
828
829impl SchedulerScopeGuardRaw {
830 pub unsafe fn enter(sched: *mut Scheduler, key: &str) -> Self {
835 unsafe {
836 (*sched).enter_scope(key);
837 }
838 Self { sched }
839 }
840}
841
842impl Drop for SchedulerScopeGuardRaw {
843 fn drop(&mut self) {
844 unsafe {
845 (*self.sched).exit_scope();
846 }
847 }
848}
849
850impl Scheduler {
851 pub fn repose<F>(
852 &mut self,
853 mut build_root: F,
854 layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
855 ) -> Frame
856 where
857 F: FnMut(&mut Scheduler) -> View,
858 {
859 let guard = ComposeGuard::begin();
860 let root = guard.scope.run(|| build_root(self));
861 let (scene, hits, sem) = layout_paint(&root, self.size);
862
863 let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
864
865 Frame {
866 scene,
867 hit_regions: hits,
868 semantics_nodes: sem,
869 focus_chain,
870 }
871 }
872}
873
874#[cfg(test)]
876pub fn clear_composer() {
877 COMPOSER.with(|c| {
878 let mut c = c.borrow_mut();
879 c.slots.clear();
880 c.slot_callers.clear();
881 c.keyed_slots.clear();
882 c.scope_caches.clear();
883 c.cursor = 0;
884 });
885 ROOT_SCOPE.with(|rs| {
886 *rs.borrow_mut() = None;
887 });
888}
889
890#[cfg(test)]
891mod focus_trap_tests {
892 use super::*;
893
894 fn region(id: u64, x: f32, group: Option<u64>) -> HitRegion {
895 HitRegion {
896 id,
897 rect: Rect {
898 x,
899 y: 0.0,
900 w: 10.0,
901 h: 10.0,
902 },
903 focus_group_id: group,
904 ..Default::default()
905 }
906 }
907
908 #[test]
909 fn arrows_stay_inside_group() {
910 let chain = vec![1, 2, 3, 4];
913 let regions = vec![
914 region(1, 0.0, None),
915 region(2, 20.0, Some(9)),
916 region(3, 40.0, Some(9)),
917 region(4, 60.0, None),
918 ];
919 assert_eq!(
920 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Left),
921 None,
922 "outsider 1 is left of 2 but outside the group: trapped"
923 );
924 assert_eq!(
925 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Right),
926 Some(3)
927 );
928 assert_eq!(
929 spatial_focus_next(&chain, ®ions, Some(3), FocusDirection::Left),
930 Some(2)
931 );
932 assert_eq!(
933 spatial_focus_next(&chain, ®ions, Some(1), FocusDirection::Right),
934 Some(2),
935 "ungrouped focus still sees the full chain"
936 );
937 }
938
939 #[test]
940 fn tab_cycles_inside_group() {
941 let chain = vec![1, 2, 3, 4];
942 let regions = vec![
943 region(1, 0.0, None),
944 region(2, 20.0, Some(9)),
945 region(3, 40.0, Some(9)),
946 region(4, 60.0, None),
947 ];
948 let mut fm = FocusManager::new(chain, Some(2));
949 fm.hit_regions = regions;
950 assert_eq!(fm.move_tab(false), Some(3));
951 assert_eq!(fm.move_tab(false), Some(2));
952 assert_eq!(fm.move_tab(true), Some(3));
953 }
954
955 #[test]
956 fn tab_from_outside_can_enter_group() {
957 let chain = vec![1, 2, 3, 4];
958 let regions = vec![
959 region(1, 0.0, None),
960 region(2, 20.0, Some(9)),
961 region(3, 40.0, Some(9)),
962 region(4, 60.0, None),
963 ];
964 let mut fm = FocusManager::new(chain, Some(1));
965 fm.hit_regions = regions;
966 assert_eq!(fm.move_tab(false), Some(2));
967 let mut fm = FocusManager::new(vec![1, 2, 3, 4], Some(1));
968 fm.hit_regions = vec![
969 region(1, 0.0, None),
970 region(2, 20.0, Some(9)),
971 region(3, 40.0, Some(9)),
972 region(4, 60.0, None),
973 ];
974 assert_eq!(fm.move_tab(true), Some(4));
975 }
976
977 #[test]
978 fn empty_group_never_moves() {
979 let chain = vec![1, 4];
980 let regions = vec![region(1, 0.0, None), region(4, 60.0, None)];
981 let mut fm = FocusManager::new(chain, Some(1));
982 fm.hit_regions = regions.clone();
983 assert_eq!(fm.move_tab(false), Some(4));
984 let chain = vec![1, 2, 4];
985 let regions = vec![
986 region(1, 0.0, None),
987 region(2, 20.0, Some(77)),
988 region(4, 60.0, None),
989 ];
990 let mut fm = FocusManager::new(chain, Some(2));
991 fm.hit_regions = regions;
992 assert_eq!(fm.move_tab(false), Some(2));
993 assert_eq!(
994 spatial_focus_next(&fm.chain, &fm.hit_regions, Some(2), FocusDirection::Right),
995 None
996 );
997 }
998}