1use std::any::Any;
2use std::cell::{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_REQUEST: Cell<Option<u64>> = const { Cell::new(None) };
20}
21
22pub const CLEAR_FOCUS_MARKER: u64 = u64::MAX;
24
25static COMPONENT_ID: AtomicU64 = AtomicU64::new(1);
28
29pub fn unique_component_id() -> u64 {
31 COMPONENT_ID.fetch_add(1, Ordering::Relaxed)
32}
33
34pub fn take_focus_request() -> Option<u64> {
35 FOCUS_REQUEST.with(|r| r.replace(None))
36}
37
38#[derive(Clone)]
44pub struct FocusRequester {
45 pub target: Rc<RefCell<Option<u64>>>,
47}
48
49impl FocusRequester {
50 pub fn new() -> Self {
51 Self {
52 target: Rc::new(RefCell::new(None)),
53 }
54 }
55
56 pub fn request_focus(&self) {
58 if let Some(id) = *self.target.borrow() {
59 FOCUS_REQUEST.with(|r| r.set(Some(id)));
60 }
61 }
62
63 pub fn free_focus(&self) {
67 FOCUS_REQUEST.with(|r| r.set(Some(CLEAR_FOCUS_MARKER)));
68 }
69
70 pub fn capture_focus(&self) {
75 self.request_focus();
76 }
77}
78
79impl Default for FocusRequester {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum FocusDirection {
88 Next,
89 Previous,
90 Left,
91 Right,
92 Up,
93 Down,
94}
95
96#[derive(Clone)]
101pub struct FocusManager {
102 pub chain: Vec<u64>,
104 pub focused: Option<u64>,
106 pub hit_regions: Vec<HitRegion>,
108}
109
110impl FocusManager {
111 pub fn new(chain: Vec<u64>, focused: Option<u64>) -> Self {
112 Self {
113 chain,
114 focused,
115 hit_regions: Vec::new(),
116 }
117 }
118
119 pub fn move_focus(&mut self, dir: FocusDirection) -> Option<u64> {
122 match dir {
123 FocusDirection::Next | FocusDirection::Previous => {
124 self.move_tab(dir == FocusDirection::Previous)
125 }
126 _ => None, }
128 }
129
130 pub fn clear_focus(&self, _force: bool) {
135 FOCUS_REQUEST.with(|r| r.set(Some(CLEAR_FOCUS_MARKER)));
136 }
137
138 pub fn move_focus_spatial(
141 &mut self,
142 dir: FocusDirection,
143 hit_regions: &[HitRegion],
144 ) -> Option<u64> {
145 let next = spatial_focus_next(&self.chain, hit_regions, self.focused, dir)?;
146 self.focused = Some(next);
147 Some(next)
148 }
149
150 pub fn move_tab(&mut self, reverse: bool) -> Option<u64> {
154 if self.chain.is_empty() {
155 return None;
156 }
157 let next = if let Some(sub_chain) =
158 focus_group_chain(&self.chain, &self.hit_regions, self.focused)
159 {
160 if sub_chain.is_empty() {
161 return None;
162 }
163 if let Some(cur) = self.focused {
164 if let Some(idx) = sub_chain.iter().position(|&id| id == cur) {
165 if reverse {
166 if idx == 0 {
167 sub_chain[sub_chain.len() - 1]
168 } else {
169 sub_chain[idx - 1]
170 }
171 } else {
172 sub_chain[(idx + 1) % sub_chain.len()]
173 }
174 } else {
175 sub_chain[0]
176 }
177 } else if reverse {
178 sub_chain[sub_chain.len() - 1]
179 } else {
180 sub_chain[0]
181 }
182 } else {
183 if let Some(cur) = self.focused {
184 if let Some(idx) = self.chain.iter().position(|&id| id == cur) {
185 if reverse {
186 if idx == 0 {
187 self.chain[self.chain.len() - 1]
188 } else {
189 self.chain[idx - 1]
190 }
191 } else {
192 self.chain[(idx + 1) % self.chain.len()]
193 }
194 } else {
195 self.chain[0]
196 }
197 } else if reverse {
198 self.chain[self.chain.len() - 1]
199 } else {
200 self.chain[0]
201 }
202 };
203 self.focused = Some(next);
204 Some(next)
205 }
206
207 pub fn set_requester_target(requester: &FocusRequester, id: u64) {
209 *requester.target.borrow_mut() = Some(id);
210 }
211}
212
213pub fn focus_group_chain(
217 chain: &[u64],
218 hit_regions: &[HitRegion],
219 current: Option<u64>,
220) -> Option<Vec<u64>> {
221 let cur = current?;
222 let group_id = hit_regions.iter().find(|h| h.id == cur)?.focus_group_id?;
223 Some(
224 chain
225 .iter()
226 .copied()
227 .filter(|&id| {
228 id == group_id
229 || hit_regions
230 .iter()
231 .any(|h| h.id == id && h.focus_group_id == Some(group_id))
232 })
233 .collect(),
234 )
235}
236
237pub fn spatial_focus_next(
242 chain: &[u64],
243 hit_regions: &[HitRegion],
244 current: Option<u64>,
245 dir: FocusDirection,
246) -> Option<u64> {
247 if chain.is_empty() {
248 return None;
249 }
250
251 let current_rect =
252 current.and_then(|id| hit_regions.iter().find(|h| h.id == id).map(|h| h.rect));
253
254 match dir {
256 FocusDirection::Next | FocusDirection::Previous => {
257 let mut fm = FocusManager {
258 chain: chain.to_vec(),
259 focused: current,
260 hit_regions: hit_regions.to_vec(),
261 };
262 return fm.move_tab(dir == FocusDirection::Previous);
263 }
264 _ => {}
265 }
266
267 let (cx, cy) = match current_rect {
268 Some(r) => (r.x + r.w / 2.0, r.y + r.h / 2.0),
269 None => {
270 return None;
272 }
273 };
274
275 let mut best: Option<(u64, f32)> = None;
276
277 let scoped: Vec<u64>;
279 let chain: &[u64] = match focus_group_chain(chain, hit_regions, current) {
280 Some(sub) => {
281 scoped = sub;
282 &scoped
283 }
284 None => chain,
285 };
286
287 for &id in chain {
288 if Some(id) == current {
289 continue;
290 }
291 let Some(hr) = hit_regions.iter().find(|h| h.id == id) else {
292 continue;
293 };
294 let r = hr.rect;
295 let other_cx = r.x + r.w / 2.0;
296 let other_cy = r.y + r.h / 2.0;
297 let dx = other_cx - cx;
298 let dy = other_cy - cy;
299
300 let in_direction = match dir {
301 FocusDirection::Left => dx < 0.0 && dy.abs() <= r.h.max(1.0),
302 FocusDirection::Right => dx > 0.0 && dy.abs() <= r.h.max(1.0),
303 FocusDirection::Up => dy < 0.0 && dx.abs() <= r.w.max(1.0),
304 FocusDirection::Down => dy > 0.0 && dx.abs() <= r.w.max(1.0),
305 _ => false,
306 };
307
308 if !in_direction {
309 continue;
310 }
311
312 let dist = dx * dx + dy * dy;
313 let weight = dist / (r.w * r.h + 1.0).max(1.0);
314
315 match best {
316 Some((_, best_weight)) if weight >= best_weight => {}
317 _ => best = Some((id, weight)),
318 }
319 }
320
321 best.map(|(id, _)| id)
322}
323
324#[derive(Default)]
325pub struct Composer {
326 pub slots: Vec<Box<dyn Any>>,
327 pub slot_callers: Vec<&'static Location<'static>>,
330 pub cursor: usize,
331 pub keyed_slots: FxHashMap<String, Box<dyn Any>>,
332 pub scope_caches: FxHashMap<String, crate::scope_cache::ScopeCache>,
335}
336
337pub struct ComposeGuard {
338 scope: Scope,
339}
340
341impl ComposeGuard {
342 pub fn begin() -> Self {
343 COMPOSER.with(|c| c.borrow_mut().cursor = 0);
344
345 let scope = ROOT_SCOPE.with(|rs| {
346 if let Some(existing) = rs.borrow().clone() {
347 existing
348 } else {
349 let s = Scope::new();
350 *rs.borrow_mut() = Some(s.clone());
351 s
352 }
353 });
354
355 ComposeGuard { scope }
356 }
357
358 pub fn scope(&self) -> &Scope {
359 &self.scope
360 }
361}
362
363impl Drop for ComposeGuard {
364 fn drop(&mut self) {
365 }
369}
370
371#[track_caller]
374pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
375 let caller = Location::caller();
378 COMPOSER.with(|c| {
379 let mut c = c.borrow_mut();
380 let cursor = c.cursor;
381 c.cursor += 1;
382
383 if cursor >= c.slots.len() {
384 let rc: Rc<T> = Rc::new(init());
385 c.slots.push(Box::new(rc.clone()));
386 c.slot_callers.push(caller);
387 return rc;
388 }
389
390 let stored_caller = c.slot_callers.get(cursor).copied();
391 if stored_caller != Some(caller) {
392 let rc: Rc<T> = Rc::new(init());
393 c.slots[cursor] = Box::new(rc.clone());
394 if cursor < c.slot_callers.len() {
395 c.slot_callers[cursor] = caller;
396 } else {
397 c.slot_callers.push(caller);
398 }
399 return rc;
400 }
401
402 if let Some(rc) = c.slots[cursor].downcast_ref::<Rc<T>>() {
403 rc.clone()
404 } else {
405 log::warn!(
406 "remember: slot {} type changed {}. \
407 Use remember_with_key(key, || ...) for conditional branches.",
408 cursor,
409 std::any::type_name::<T>(),
410 );
411 let rc: Rc<T> = Rc::new(init());
412 c.slots[cursor] = Box::new(rc.clone());
413 rc
414 }
415 })
416}
417
418pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
420 COMPOSER.with(|c| {
421 let mut c = c.borrow_mut();
422 let key = key.into();
423
424 if let Some(existing) = c.keyed_slots.get(&key) {
425 if let Some(rc) = existing.downcast_ref::<Rc<T>>() {
426 return rc.clone();
427 } else {
428 log::warn!(
429 "remember_with_key: key '{}' reused with a different type; replacing.",
430 key
431 );
432 }
433 }
434
435 if cfg!(debug_assertions) && c.keyed_slots.len() > 10_000 {
436 log::warn!(
437 "remember_with_key: more than 10k keys stored; \
438 are you generating unbounded dynamic keys (e.g., using timestamps)?"
439 );
440 }
441
442 let rc: Rc<T> = Rc::new(init());
443 c.keyed_slots.insert(key, Box::new(rc.clone()));
444 rc
445 })
446}
447
448#[track_caller]
451pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
452 remember(|| RefCell::new(init()))
453}
454
455pub fn remember_state_with_key<T: 'static>(
457 key: impl Into<String>,
458 init: impl FnOnce() -> T,
459) -> Rc<RefCell<T>> {
460 remember_with_key(key, || RefCell::new(init()))
461}
462
463#[derive(Clone)]
465pub struct Frame {
466 pub scene: Scene,
467 pub hit_regions: Vec<HitRegion>,
468 pub semantics_nodes: Vec<SemNode>,
469 pub focus_chain: Vec<u64>,
470}
471
472#[derive(Clone, Default)]
475pub struct HitRegion {
476 pub id: u64,
477 pub rect: Rect,
478 pub depth: u32,
481 pub parent: Option<u64>,
482 pub on_click: Option<Rc<dyn Fn()>>,
483 pub on_double_click: Option<Rc<dyn Fn()>>,
484 pub on_long_click: Option<Rc<dyn Fn()>>,
485 pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
486 pub focusable: bool,
487 pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
488 pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
489 pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
490 pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
491 pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
492 pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
493 pub z_index: f32,
494 pub disabled: bool,
495 pub on_text_change: Option<Rc<dyn Fn(String)>>,
496 pub on_text_submit: Option<Rc<dyn Fn(String)>>,
497 pub tf_state_key: Option<u64>,
500
501 pub tf_multiline: bool,
503
504 pub tf_content_origin: Option<(f32, f32)>,
509
510 pub tf_enabled: bool,
512
513 pub tf_read_only: bool,
515
516 pub tf_value: String,
518
519 pub tf_font_size: crate::units::Sp,
522
523 pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
525 pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
526 pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
527 pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
528 pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
529 pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
530 pub drag_preview: Option<crate::dnd::DragPreview>,
532
533 pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
534
535 pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
538 pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
540
541 pub cursor: Option<crate::CursorIcon>,
543
544 pub focus_group_id: Option<u64>,
549
550 pub keyboard_type: crate::text::KeyboardType,
553 pub capitalization: crate::text::KeyboardCapitalization,
554 pub ime_action: crate::text::ImeAction,
555 pub auto_correct: Option<bool>,
558
559 pub interaction_source: Option<crate::modifier::InteractionSource>,
564}
565
566impl HitRegion {
567 pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
571 Self {
572 id,
573 rect,
574 z_index: m.z_index,
575 on_click: m.on_click.clone(),
576 on_double_click: m.on_double_click.clone(),
577 on_long_click: m.on_long_click.clone(),
578 on_pointer_down: m.on_pointer_down.clone(),
579 on_pointer_move: m.on_pointer_move.clone(),
580 on_pointer_up: m.on_pointer_up.clone(),
581 on_pointer_cancel: m.on_pointer_cancel.clone(),
582 on_pointer_enter: m.on_pointer_enter.clone(),
583 on_pointer_leave: m.on_pointer_leave.clone(),
584 on_action: m.on_action.clone(),
585 on_key_event: m.on_key_event.clone(),
586 on_preview_key_event: m.on_preview_key_event.clone(),
587 cursor: m.cursor,
588 on_drag_start: m.on_drag_start.clone(),
589 on_drag_end: m.on_drag_end.clone(),
590 on_drag_enter: m.on_drag_enter.clone(),
591 on_drag_over: m.on_drag_over.clone(),
592 on_drag_leave: m.on_drag_leave.clone(),
593 on_scroll: m.on_scroll.clone(),
594 on_drop: m.on_drop.clone(),
595 drag_preview: m.drag_preview.clone(),
596 disabled: m.disabled,
597 tf_enabled: true,
598 tf_read_only: false,
599 ..Default::default()
600 }
601 }
602}
603
604#[derive(Clone)]
612pub struct SemNode {
613 pub id: u64,
615
616 pub parent: Option<u64>,
618
619 pub role: Role,
620 pub label: Option<String>,
621 pub rect: Rect,
622 pub focused: bool,
623 pub enabled: bool,
624 pub selectable_group: bool,
626 pub checked: Option<bool>,
627 pub selected: Option<bool>,
628 pub value: Option<String>,
629}
630
631impl Default for SemNode {
632 fn default() -> Self {
633 Self {
634 id: 0,
635 parent: None,
636 role: Role::default(),
637 label: None,
638 rect: Rect::default(),
639 focused: false,
640 enabled: true,
641 selectable_group: false,
642 checked: None,
643 selected: None,
644 value: None,
645 }
646 }
647}
648
649pub struct Scheduler {
650 next_id: u64,
651 scope_key_to_id: FxHashMap<String, u32>,
654 next_scope_id: u32,
655 current_scope: Option<String>,
659 scope_local_counters: FxHashMap<String, u32>,
661 pub focused: Option<u64>,
662 pub size: (u32, u32),
663}
664
665impl Default for Scheduler {
666 fn default() -> Self {
667 Self::new()
668 }
669}
670
671impl Scheduler {
672 pub fn new() -> Self {
673 Self {
674 next_id: 1,
675 scope_key_to_id: FxHashMap::default(),
676 next_scope_id: 1,
677 current_scope: None,
678 scope_local_counters: FxHashMap::default(),
679 focused: None,
680 size: (1280, 800),
681 }
682 }
683
684 pub fn enter_scope(&mut self, key: &str) {
689 self.current_scope = Some(key.to_string());
690 self.scope_local_counters.insert(key.to_string(), 0);
692 self.get_or_create_scope_id(key);
694 }
695
696 pub fn exit_scope(&mut self) {
698 self.current_scope = None;
699 }
700
701 fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
702 if let Some(&id) = self.scope_key_to_id.get(key) {
703 id
704 } else {
705 let id = self.next_scope_id;
706 self.next_scope_id += 1;
707 self.scope_key_to_id.insert(key.to_string(), id);
708 id
709 }
710 }
711
712 pub fn id(&mut self) -> u64 {
713 if let Some(key) = &self.current_scope {
714 let scope_id = self.scope_key_to_id.get(key).copied().unwrap_or(0);
716 let local = self.scope_local_counters.get_mut(key).unwrap();
717 let id = *local;
718 *local += 1;
719 (scope_id as u64) << 32 | id as u64
720 } else {
721 let id = self.next_id;
723 self.next_id += 1;
724 id
725 }
726 }
727
728 pub fn id_count(&self) -> u64 {
729 self.next_id - 1
730 }
731
732 pub fn snapshot_id(&self) -> u64 {
735 self.next_id
736 }
737
738 pub fn advance_id(&mut self, count: u32) {
741 self.next_id += count as u64;
742 }
743
744 pub fn ids_used_since(&self, prev_id: u64) -> u32 {
747 (self.next_id - prev_id) as u32
748 }
749
750 pub fn repose<F>(
751 &mut self,
752 mut build_root: F,
753 layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
754 ) -> Frame
755 where
756 F: FnMut(&mut Scheduler) -> View,
757 {
758 let guard = ComposeGuard::begin();
759 let root = guard.scope.run(|| build_root(self));
760 let (scene, hits, sem) = layout_paint(&root, self.size);
761
762 let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
763
764 Frame {
765 scene,
766 hit_regions: hits,
767 semantics_nodes: sem,
768 focus_chain,
769 }
770 }
771}
772
773#[cfg(test)]
775pub fn clear_composer() {
776 COMPOSER.with(|c| {
777 let mut c = c.borrow_mut();
778 c.slots.clear();
779 c.slot_callers.clear();
780 c.keyed_slots.clear();
781 c.scope_caches.clear();
782 c.cursor = 0;
783 });
784 ROOT_SCOPE.with(|rs| {
785 *rs.borrow_mut() = None;
786 });
787}
788
789#[cfg(test)]
790mod focus_trap_tests {
791 use super::*;
792
793 fn region(id: u64, x: f32, group: Option<u64>) -> HitRegion {
794 HitRegion {
795 id,
796 rect: Rect {
797 x,
798 y: 0.0,
799 w: 10.0,
800 h: 10.0,
801 },
802 focus_group_id: group,
803 ..Default::default()
804 }
805 }
806
807 #[test]
808 fn arrows_stay_inside_group() {
809 let chain = vec![1, 2, 3, 4];
812 let regions = vec![
813 region(1, 0.0, None),
814 region(2, 20.0, Some(9)),
815 region(3, 40.0, Some(9)),
816 region(4, 60.0, None),
817 ];
818 assert_eq!(
819 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Left),
820 None,
821 "outsider 1 is left of 2 but outside the group: trapped"
822 );
823 assert_eq!(
824 spatial_focus_next(&chain, ®ions, Some(2), FocusDirection::Right),
825 Some(3)
826 );
827 assert_eq!(
828 spatial_focus_next(&chain, ®ions, Some(3), FocusDirection::Left),
829 Some(2)
830 );
831 assert_eq!(
832 spatial_focus_next(&chain, ®ions, Some(1), FocusDirection::Right),
833 Some(2),
834 "ungrouped focus still sees the full chain"
835 );
836 }
837
838 #[test]
839 fn tab_cycles_inside_group() {
840 let chain = vec![1, 2, 3, 4];
841 let regions = vec![
842 region(1, 0.0, None),
843 region(2, 20.0, Some(9)),
844 region(3, 40.0, Some(9)),
845 region(4, 60.0, None),
846 ];
847 let mut fm = FocusManager::new(chain, Some(2));
848 fm.hit_regions = regions;
849 assert_eq!(fm.move_tab(false), Some(3));
850 assert_eq!(fm.move_tab(false), Some(2));
851 assert_eq!(fm.move_tab(true), Some(3));
852 }
853
854 #[test]
855 fn tab_from_outside_can_enter_group() {
856 let chain = vec![1, 2, 3, 4];
857 let regions = vec![
858 region(1, 0.0, None),
859 region(2, 20.0, Some(9)),
860 region(3, 40.0, Some(9)),
861 region(4, 60.0, None),
862 ];
863 let mut fm = FocusManager::new(chain, Some(1));
864 fm.hit_regions = regions;
865 assert_eq!(fm.move_tab(false), Some(2));
866 let mut fm = FocusManager::new(vec![1, 2, 3, 4], Some(1));
867 fm.hit_regions = vec![
868 region(1, 0.0, None),
869 region(2, 20.0, Some(9)),
870 region(3, 40.0, Some(9)),
871 region(4, 60.0, None),
872 ];
873 assert_eq!(fm.move_tab(true), Some(4));
874 }
875
876 #[test]
877 fn empty_group_never_moves() {
878 let chain = vec![1, 4];
879 let regions = vec![region(1, 0.0, None), region(4, 60.0, None)];
880 let mut fm = FocusManager::new(chain, Some(1));
881 fm.hit_regions = regions.clone();
882 assert_eq!(fm.move_tab(false), Some(4));
883 let chain = vec![1, 2, 4];
884 let regions = vec![
885 region(1, 0.0, None),
886 region(2, 20.0, Some(77)),
887 region(4, 60.0, None),
888 ];
889 let mut fm = FocusManager::new(chain, Some(2));
890 fm.hit_regions = regions;
891 assert_eq!(fm.move_tab(false), Some(2));
892 assert_eq!(
893 spatial_focus_next(&fm.chain, &fm.hit_regions, Some(2), FocusDirection::Right),
894 None
895 );
896 }
897}