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, input::PhysicalKey, 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>>,
338 pub cursor: usize,
339 pub keyed_slots: FxHashMap<String, Box<dyn Any>>,
340 pub keyed_owner: FxHashMap<String, String>,
341 pub live_keyed_owners: rustc_hash::FxHashSet<String>,
342 pub scope_caches: FxHashMap<String, crate::scope_cache::ScopeCache>,
343 pub live_scope_keys: rustc_hash::FxHashSet<String>,
344}
345
346pub struct ComposeGuard {
347 scope: Scope,
348}
349
350pub(crate) fn current_scope_key_for_remember() -> Option<String> {
351 crate::scope_cache::current_scope_key()
352}
353
354impl ComposeGuard {
355 pub fn begin() -> Self {
356 COMPOSER.with(|c| {
357 let mut c = c.borrow_mut();
358 c.cursor = 0;
359 c.live_scope_keys.clear();
360 c.live_keyed_owners.clear();
361 c.live_scope_keys.insert(String::new());
362 c.live_keyed_owners.insert(String::new());
363 });
364
365 let scope = ROOT_SCOPE.with(|rs| {
366 if let Some(existing) = rs.borrow().clone() {
367 existing
368 } else {
369 let s = Scope::new();
370 *rs.borrow_mut() = Some(s.clone());
371 s
372 }
373 });
374
375 ComposeGuard { scope }
376 }
377
378 pub fn scope(&self) -> &Scope {
379 &self.scope
380 }
381}
382
383impl Drop for ComposeGuard {
384 fn drop(&mut self) {
385 COMPOSER.with(|c| {
386 let mut c = c.borrow_mut();
387 let n = c.cursor;
388 if c.slots.len() > n {
389 c.slots.truncate(n);
390 }
391 if c.slot_callers.len() > n {
392 c.slot_callers.truncate(n);
393 }
394 });
395 crate::scope_cache::gc_dead_scopes();
396 }
397}
398
399pub fn shutdown_composition() {
404 ROOT_SCOPE.with(|rs| {
405 if let Some(scope) = rs.borrow_mut().take() {
406 scope.dispose();
407 }
408 });
409 COMPOSER.with(|c| {
410 let mut c = c.borrow_mut();
411 c.slots.clear();
412 c.slot_callers.clear();
413 c.keyed_slots.clear();
414 c.keyed_owner.clear();
415 c.live_keyed_owners.clear();
416 c.scope_caches.clear();
417 c.live_scope_keys.clear();
418 c.cursor = 0;
419 });
420 crate::scope_cache::clear_all_scope_deps();
421}
422
423#[track_caller]
426pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
427 let caller = Location::caller();
430 COMPOSER.with(|c| {
431 let mut c = c.borrow_mut();
432 let cursor = c.cursor;
433 c.cursor += 1;
434
435 if cursor >= c.slots.len() {
436 let rc: Rc<T> = Rc::new(init());
437 c.slots.push(Box::new(rc.clone()));
438 c.slot_callers.push(caller);
439 return rc;
440 }
441
442 let stored_caller = c.slot_callers.get(cursor).copied();
443 if stored_caller != Some(caller) {
444 let rc: Rc<T> = Rc::new(init());
445 c.slots[cursor] = Box::new(rc.clone());
446 if cursor < c.slot_callers.len() {
447 c.slot_callers[cursor] = caller;
448 } else {
449 c.slot_callers.push(caller);
450 }
451 return rc;
452 }
453
454 if let Some(rc) = c.slots[cursor].downcast_ref::<Rc<T>>() {
455 rc.clone()
456 } else {
457 log::warn!(
458 "remember: slot {} type changed {}. \
459 Use remember_with_key(key, || ...) for conditional branches.",
460 cursor,
461 std::any::type_name::<T>(),
462 );
463 let rc: Rc<T> = Rc::new(init());
464 c.slots[cursor] = Box::new(rc.clone());
465 rc
466 }
467 })
468}
469
470pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
472 let owner = current_scope_key_for_remember().unwrap_or_default();
473 COMPOSER.with(|c| {
474 let mut c = c.borrow_mut();
475 let key = key.into();
476
477 if let Some(existing) = c.keyed_slots.get(&key) {
478 if let Some(rc) = existing.downcast_ref::<Rc<T>>() {
479 let rc = rc.clone();
480 c.keyed_owner.insert(key.clone(), owner.clone());
481 c.live_keyed_owners.insert(owner);
482 return rc;
483 } else {
484 log::warn!(
485 "remember_with_key: key '{}' reused with a different type; replacing.",
486 key
487 );
488 }
489 }
490
491 if cfg!(debug_assertions) && c.keyed_slots.len() > 10_000 {
492 log::warn!(
493 "remember_with_key: more than 10k keys stored; \
494 are you generating unbounded dynamic keys (e.g., using timestamps)?"
495 );
496 }
497
498 let rc: Rc<T> = Rc::new(init());
499 c.keyed_slots.insert(key.clone(), Box::new(rc.clone()));
500 c.keyed_owner.insert(key, owner.clone());
501 c.live_keyed_owners.insert(owner);
502 rc
503 })
504}
505
506#[track_caller]
509pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
510 remember(|| RefCell::new(init()))
511}
512
513pub fn remember_state_with_key<T: 'static>(
515 key: impl Into<String>,
516 init: impl FnOnce() -> T,
517) -> Rc<RefCell<T>> {
518 remember_with_key(key, || RefCell::new(init()))
519}
520
521#[derive(Clone)]
523pub struct Frame {
524 pub scene: Scene,
525 pub hit_regions: Vec<HitRegion>,
526 pub semantics_nodes: Vec<SemNode>,
527 pub focus_chain: Vec<u64>,
528}
529
530#[derive(Clone, Default)]
533pub struct HitRegion {
534 pub id: u64,
535 pub rect: Rect,
536 pub depth: u32,
539 pub parent: Option<u64>,
540 pub on_click: Option<Rc<dyn Fn()>>,
541 pub on_double_click: Option<Rc<dyn Fn()>>,
542 pub on_long_click: Option<Rc<dyn Fn()>>,
543 pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
544 pub focusable: bool,
545 pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
546 pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
547 pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
548 pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
549 pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
550 pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
551 pub z_index: f32,
552 pub disabled: bool,
553 pub on_text_change: Option<Rc<dyn Fn(String)>>,
554 pub on_text_submit: Option<Rc<dyn Fn(String)>>,
555 pub tf_state_key: Option<u64>,
558
559 pub tf_multiline: bool,
561
562 pub tf_content_origin: Option<(f32, f32)>,
567
568 pub tf_enabled: bool,
570
571 pub tf_read_only: bool,
573
574 pub tf_value: String,
576
577 pub tf_font_size: crate::units::Sp,
580
581 pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
583 pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
584 pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
585 pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
586 pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
587 pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
588 pub drag_preview: Option<crate::dnd::DragPreview>,
590
591 pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
592
593 pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
596 pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
598
599 pub cursor: Option<crate::CursorIcon>,
601
602 pub focus_group_id: Option<u64>,
607
608 pub keyboard_type: crate::text::KeyboardType,
611 pub capitalization: crate::text::KeyboardCapitalization,
612 pub ime_action: crate::text::ImeAction,
613 pub auto_correct: Option<bool>,
616
617 pub interaction_source: Option<crate::modifier::InteractionSource>,
622}
623
624impl HitRegion {
625 pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
629 Self {
630 id,
631 rect,
632 z_index: m.z_index,
633 on_click: m.on_click.clone(),
634 on_double_click: m.on_double_click.clone(),
635 on_long_click: m.on_long_click.clone(),
636 on_pointer_down: m.on_pointer_down.clone(),
637 on_pointer_move: m.on_pointer_move.clone(),
638 on_pointer_up: m.on_pointer_up.clone(),
639 on_pointer_cancel: m.on_pointer_cancel.clone(),
640 on_pointer_enter: m.on_pointer_enter.clone(),
641 on_pointer_leave: m.on_pointer_leave.clone(),
642 on_action: m.on_action.clone(),
643 on_key_event: m.on_key_event.clone(),
644 on_preview_key_event: m.on_preview_key_event.clone(),
645 cursor: m.cursor.clone(),
646 on_drag_start: m.on_drag_start.clone(),
647 on_drag_end: m.on_drag_end.clone(),
648 on_drag_enter: m.on_drag_enter.clone(),
649 on_drag_over: m.on_drag_over.clone(),
650 on_drag_leave: m.on_drag_leave.clone(),
651 on_scroll: m.on_scroll.clone(),
652 on_drop: m.on_drop.clone(),
653 drag_preview: m.drag_preview.clone(),
654 disabled: m.disabled,
655 tf_enabled: true,
656 tf_read_only: false,
657 ..Default::default()
658 }
659 }
660}
661
662#[derive(Clone)]
670pub struct SemNode {
671 pub id: u64,
673
674 pub parent: Option<u64>,
676
677 pub role: Role,
678 pub label: Option<String>,
679 pub rect: Rect,
680 pub focused: bool,
681 pub enabled: bool,
682 pub selectable_group: bool,
684 pub checked: Option<bool>,
685 pub selected: Option<bool>,
686 pub value: Option<String>,
687}
688
689impl Default for SemNode {
690 fn default() -> Self {
691 Self {
692 id: 0,
693 parent: None,
694 role: Role::default(),
695 label: None,
696 rect: Rect::default(),
697 focused: false,
698 enabled: true,
699 selectable_group: false,
700 checked: None,
701 selected: None,
702 value: None,
703 }
704 }
705}
706
707pub struct Scheduler {
708 next_id: u64,
709 scope_key_to_id: FxHashMap<String, u32>,
712 next_scope_id: u32,
713 current_scope: Vec<String>,
718 scope_local_counters: FxHashMap<String, u32>,
720 pub focused: Option<u64>,
721 pub size: (u32, u32),
722 pub pointer_pos_px: Option<(f32, f32)>,
725 pub held_keys: HashSet<PhysicalKey>,
732 pub window_focused: bool,
735 pub touch_points: Vec<(u64, f32, f32)>,
742 pub mouse_primary: bool,
746 pub mouse_secondary: bool,
747 pub mouse_middle: bool,
748 pub cursor_override: Option<CursorIcon>,
756}
757
758impl Default for Scheduler {
759 fn default() -> Self {
760 Self::new()
761 }
762}
763
764impl Scheduler {
765 pub fn new() -> Self {
766 Self {
767 next_id: 1,
768 scope_key_to_id: FxHashMap::default(),
769 next_scope_id: 1,
770 current_scope: Vec::new(),
771 scope_local_counters: FxHashMap::default(),
772 focused: None,
773 size: (1280, 800),
774 pointer_pos_px: None,
775 held_keys: HashSet::new(),
776 window_focused: true,
777 touch_points: Vec::new(),
778 mouse_primary: false,
779 mouse_secondary: false,
780 mouse_middle: false,
781 cursor_override: None,
782 }
783 }
784
785 pub fn enter_scope(&mut self, key: &str) {
790 if !self.current_scope.iter().any(|k| k == key) {
791 self.scope_local_counters.insert(key.to_string(), 0);
792 }
793 self.current_scope.push(key.to_string());
794 self.get_or_create_scope_id(key);
795 }
796
797 pub fn exit_scope(&mut self) {
800 self.current_scope.pop();
801 }
802
803 pub fn scope_guard<'a>(&'a mut self, key: &str) -> SchedulerScopeGuard<'a> {
807 self.enter_scope(key);
808 SchedulerScopeGuard { sched: self }
809 }
810
811 pub fn scope_guard_raw(&mut self, key: &str) -> SchedulerScopeGuardRaw {
815 let ptr = self as *mut Scheduler;
816 unsafe { SchedulerScopeGuardRaw::enter(ptr, key) }
817 }
818
819 fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
820 if let Some(&id) = self.scope_key_to_id.get(key) {
821 id
822 } else {
823 let id = self.next_scope_id;
824 self.next_scope_id += 1;
825 self.scope_key_to_id.insert(key.to_string(), id);
826 id
827 }
828 }
829
830 pub fn id(&mut self) -> u64 {
831 if let Some(key) = self.current_scope.last().cloned() {
832 let scope_id = self.scope_key_to_id.get(&key).copied().unwrap_or(0);
833 let local = self.scope_local_counters.get_mut(&key).unwrap();
834 let id = *local;
835 *local += 1;
836 (scope_id as u64) << 32 | id as u64
837 } else {
838 let id = self.next_id;
840 self.next_id += 1;
841 id
842 }
843 }
844
845 pub fn id_count(&self) -> u64 {
846 self.next_id - 1
847 }
848
849 pub fn is_held(&self, key: PhysicalKey) -> bool {
851 self.held_keys.contains(&key)
852 }
853
854 pub fn snapshot_id(&self) -> u64 {
857 self.next_id
858 }
859
860 pub fn advance_id(&mut self, count: u32) {
863 self.next_id += count as u64;
864 }
865
866 pub fn ids_used_since(&self, prev_id: u64) -> u32 {
869 (self.next_id - prev_id) as u32
870 }
871}
872
873pub struct SchedulerScopeGuard<'a> {
875 sched: &'a mut Scheduler,
876}
877
878impl Drop for SchedulerScopeGuard<'_> {
879 fn drop(&mut self) {
880 self.sched.exit_scope();
881 }
882}
883
884pub struct SchedulerScopeGuardRaw {
891 sched: *mut Scheduler,
892}
893
894impl SchedulerScopeGuardRaw {
895 pub unsafe fn enter(sched: *mut Scheduler, key: &str) -> Self {
900 unsafe {
901 (*sched).enter_scope(key);
902 }
903 Self { sched }
904 }
905}
906
907impl Drop for SchedulerScopeGuardRaw {
908 fn drop(&mut self) {
909 unsafe {
910 (*self.sched).exit_scope();
911 }
912 }
913}
914
915impl Scheduler {
916 pub fn repose<F>(
917 &mut self,
918 mut build_root: F,
919 layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
920 ) -> Frame
921 where
922 F: FnMut(&mut Scheduler) -> View,
923 {
924 let guard = ComposeGuard::begin();
925 let root = guard.scope.run(|| build_root(self));
926 let (scene, hits, sem) = layout_paint(&root, self.size);
927
928 let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
929
930 Frame {
931 scene,
932 hit_regions: hits,
933 semantics_nodes: sem,
934 focus_chain,
935 }
936 }
937}
938
939#[cfg(test)]
942pub fn clear_composer() {
943 shutdown_composition();
944}
945
946#[cfg(test)]
947mod focus_trap_tests {
948 use super::*;
949
950 fn region(id: u64, x: f32, group: Option<u64>) -> HitRegion {
951 HitRegion {
952 id,
953 rect: Rect {
954 x,
955 y: 0.0,
956 w: 10.0,
957 h: 10.0,
958 },
959 focus_group_id: group,
960 ..Default::default()
961 }
962 }
963
964 #[test]
965 fn arrows_stay_inside_group() {
966 let chain = vec![1, 2, 3, 4];
969 let regions = vec![
970 region(1, 0.0, None),
971 region(2, 20.0, Some(9)),
972 region(3, 40.0, Some(9)),
973 region(4, 60.0, None),
974 ];
975 assert_eq!(
976 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Left),
977 None,
978 "outsider 1 is left of 2 but outside the group: trapped"
979 );
980 assert_eq!(
981 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Right),
982 Some(3)
983 );
984 assert_eq!(
985 spatial_focus_next(&chain, ®ions, Some(3), FocusDirection::Left),
986 Some(2)
987 );
988 assert_eq!(
989 spatial_focus_next(&chain, ®ions, Some(1), FocusDirection::Right),
990 Some(2),
991 "ungrouped focus still sees the full chain"
992 );
993 }
994
995 #[test]
996 fn tab_cycles_inside_group() {
997 let chain = vec![1, 2, 3, 4];
998 let regions = vec![
999 region(1, 0.0, None),
1000 region(2, 20.0, Some(9)),
1001 region(3, 40.0, Some(9)),
1002 region(4, 60.0, None),
1003 ];
1004 let mut fm = FocusManager::new(chain, Some(2));
1005 fm.hit_regions = regions;
1006 assert_eq!(fm.move_tab(false), Some(3));
1007 assert_eq!(fm.move_tab(false), Some(2));
1008 assert_eq!(fm.move_tab(true), Some(3));
1009 }
1010
1011 #[test]
1012 fn tab_from_outside_can_enter_group() {
1013 let chain = vec![1, 2, 3, 4];
1014 let regions = vec![
1015 region(1, 0.0, None),
1016 region(2, 20.0, Some(9)),
1017 region(3, 40.0, Some(9)),
1018 region(4, 60.0, None),
1019 ];
1020 let mut fm = FocusManager::new(chain, Some(1));
1021 fm.hit_regions = regions;
1022 assert_eq!(fm.move_tab(false), Some(2));
1023 let mut fm = FocusManager::new(vec![1, 2, 3, 4], Some(1));
1024 fm.hit_regions = vec![
1025 region(1, 0.0, None),
1026 region(2, 20.0, Some(9)),
1027 region(3, 40.0, Some(9)),
1028 region(4, 60.0, None),
1029 ];
1030 assert_eq!(fm.move_tab(true), Some(4));
1031 }
1032
1033 #[test]
1034 fn empty_group_never_moves() {
1035 let chain = vec![1, 4];
1036 let regions = vec![region(1, 0.0, None), region(4, 60.0, None)];
1037 let mut fm = FocusManager::new(chain, Some(1));
1038 fm.hit_regions = regions.clone();
1039 assert_eq!(fm.move_tab(false), Some(4));
1040 let chain = vec![1, 2, 4];
1041 let regions = vec![
1042 region(1, 0.0, None),
1043 region(2, 20.0, Some(77)),
1044 region(4, 60.0, None),
1045 ];
1046 let mut fm = FocusManager::new(chain, Some(2));
1047 fm.hit_regions = regions;
1048 assert_eq!(fm.move_tab(false), Some(2));
1049 assert_eq!(
1050 spatial_focus_next(&fm.chain, &fm.hit_regions, Some(2), FocusDirection::Right),
1051 None
1052 );
1053 }
1054}