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 current_group = self.focused.and_then(|cur| {
158 self.hit_regions
159 .iter()
160 .find(|h| h.id == cur)
161 .and_then(|h| h.focus_group_id)
162 });
163 let next = if let Some(group_id) = current_group {
164 let sub_chain: Vec<u64> = self
166 .chain
167 .iter()
168 .copied()
169 .filter(|&id| {
170 id == group_id
171 || self
172 .hit_regions
173 .iter()
174 .any(|h| h.id == id && h.focus_group_id == Some(group_id))
175 })
176 .collect();
177 if sub_chain.is_empty() {
178 return None;
179 }
180 if let Some(cur) = self.focused {
181 if let Some(idx) = sub_chain.iter().position(|&id| id == cur) {
182 if reverse {
183 if idx == 0 {
184 sub_chain[sub_chain.len() - 1]
185 } else {
186 sub_chain[idx - 1]
187 }
188 } else {
189 sub_chain[(idx + 1) % sub_chain.len()]
190 }
191 } else {
192 sub_chain[0]
193 }
194 } else if reverse {
195 sub_chain[sub_chain.len() - 1]
196 } else {
197 sub_chain[0]
198 }
199 } else {
200 if let Some(cur) = self.focused {
201 if let Some(idx) = self.chain.iter().position(|&id| id == cur) {
202 if reverse {
203 if idx == 0 {
204 self.chain[self.chain.len() - 1]
205 } else {
206 self.chain[idx - 1]
207 }
208 } else {
209 self.chain[(idx + 1) % self.chain.len()]
210 }
211 } else {
212 self.chain[0]
213 }
214 } else if reverse {
215 self.chain[self.chain.len() - 1]
216 } else {
217 self.chain[0]
218 }
219 };
220 self.focused = Some(next);
221 Some(next)
222 }
223
224 pub fn set_requester_target(requester: &FocusRequester, id: u64) {
226 *requester.target.borrow_mut() = Some(id);
227 }
228}
229
230pub fn spatial_focus_next(
235 chain: &[u64],
236 hit_regions: &[HitRegion],
237 current: Option<u64>,
238 dir: FocusDirection,
239) -> Option<u64> {
240 if chain.is_empty() {
241 return None;
242 }
243
244 let current_rect =
245 current.and_then(|id| hit_regions.iter().find(|h| h.id == id).map(|h| h.rect));
246
247 match dir {
249 FocusDirection::Next | FocusDirection::Previous => {
250 let mut fm = FocusManager {
251 chain: chain.to_vec(),
252 focused: current,
253 hit_regions: hit_regions.to_vec(),
254 };
255 return fm.move_tab(dir == FocusDirection::Previous);
256 }
257 _ => {}
258 }
259
260 let (cx, cy) = match current_rect {
261 Some(r) => (r.x + r.w / 2.0, r.y + r.h / 2.0),
262 None => {
263 return None;
265 }
266 };
267
268 let mut best: Option<(u64, f32)> = None;
269
270 for &id in chain {
271 if Some(id) == current {
272 continue;
273 }
274 let Some(hr) = hit_regions.iter().find(|h| h.id == id) else {
275 continue;
276 };
277 let r = hr.rect;
278 let other_cx = r.x + r.w / 2.0;
279 let other_cy = r.y + r.h / 2.0;
280 let dx = other_cx - cx;
281 let dy = other_cy - cy;
282
283 let in_direction = match dir {
284 FocusDirection::Left => dx < 0.0 && dy.abs() <= r.h.max(1.0),
285 FocusDirection::Right => dx > 0.0 && dy.abs() <= r.h.max(1.0),
286 FocusDirection::Up => dy < 0.0 && dx.abs() <= r.w.max(1.0),
287 FocusDirection::Down => dy > 0.0 && dx.abs() <= r.w.max(1.0),
288 _ => false,
289 };
290
291 if !in_direction {
292 continue;
293 }
294
295 let dist = dx * dx + dy * dy;
296 let weight = dist / (r.w * r.h + 1.0).max(1.0);
297
298 match best {
299 Some((_, best_weight)) if weight >= best_weight => {}
300 _ => best = Some((id, weight)),
301 }
302 }
303
304 best.map(|(id, _)| id)
305}
306
307#[derive(Default)]
308pub struct Composer {
309 pub slots: Vec<Box<dyn Any>>,
310 pub slot_callers: Vec<&'static Location<'static>>,
313 pub cursor: usize,
314 pub keyed_slots: FxHashMap<String, Box<dyn Any>>,
315 pub scope_caches: FxHashMap<String, crate::scope_cache::ScopeCache>,
318}
319
320pub struct ComposeGuard {
321 scope: Scope,
322}
323
324impl ComposeGuard {
325 pub fn begin() -> Self {
326 COMPOSER.with(|c| c.borrow_mut().cursor = 0);
327
328 let scope = ROOT_SCOPE.with(|rs| {
329 if let Some(existing) = rs.borrow().clone() {
330 existing
331 } else {
332 let s = Scope::new();
333 *rs.borrow_mut() = Some(s.clone());
334 s
335 }
336 });
337
338 ComposeGuard { scope }
339 }
340
341 pub fn scope(&self) -> &Scope {
342 &self.scope
343 }
344}
345
346impl Drop for ComposeGuard {
347 fn drop(&mut self) {
348 }
352}
353
354#[track_caller]
357pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
358 let caller = Location::caller();
361 COMPOSER.with(|c| {
362 let mut c = c.borrow_mut();
363 let cursor = c.cursor;
364 c.cursor += 1;
365
366 if cursor >= c.slots.len() {
367 let rc: Rc<T> = Rc::new(init());
368 c.slots.push(Box::new(rc.clone()));
369 c.slot_callers.push(caller);
370 return rc;
371 }
372
373 let stored_caller = c.slot_callers.get(cursor).copied();
374 if stored_caller != Some(caller) {
375 let rc: Rc<T> = Rc::new(init());
376 c.slots[cursor] = Box::new(rc.clone());
377 if cursor < c.slot_callers.len() {
378 c.slot_callers[cursor] = caller;
379 } else {
380 c.slot_callers.push(caller);
381 }
382 return rc;
383 }
384
385 if let Some(rc) = c.slots[cursor].downcast_ref::<Rc<T>>() {
386 rc.clone()
387 } else {
388 log::warn!(
389 "remember: slot {} type changed {}. \
390 Use remember_with_key(key, || ...) for conditional branches.",
391 cursor,
392 std::any::type_name::<T>(),
393 );
394 let rc: Rc<T> = Rc::new(init());
395 c.slots[cursor] = Box::new(rc.clone());
396 rc
397 }
398 })
399}
400
401pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
403 COMPOSER.with(|c| {
404 let mut c = c.borrow_mut();
405 let key = key.into();
406
407 if let Some(existing) = c.keyed_slots.get(&key) {
408 if let Some(rc) = existing.downcast_ref::<Rc<T>>() {
409 return rc.clone();
410 } else {
411 log::warn!(
412 "remember_with_key: key '{}' reused with a different type; replacing.",
413 key
414 );
415 }
416 }
417
418 if cfg!(debug_assertions) && c.keyed_slots.len() > 10_000 {
419 log::warn!(
420 "remember_with_key: more than 10k keys stored; \
421 are you generating unbounded dynamic keys (e.g., using timestamps)?"
422 );
423 }
424
425 let rc: Rc<T> = Rc::new(init());
426 c.keyed_slots.insert(key, Box::new(rc.clone()));
427 rc
428 })
429}
430
431#[track_caller]
434pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
435 remember(|| RefCell::new(init()))
436}
437
438pub fn remember_state_with_key<T: 'static>(
440 key: impl Into<String>,
441 init: impl FnOnce() -> T,
442) -> Rc<RefCell<T>> {
443 remember_with_key(key, || RefCell::new(init()))
444}
445
446#[derive(Clone)]
448pub struct Frame {
449 pub scene: Scene,
450 pub hit_regions: Vec<HitRegion>,
451 pub semantics_nodes: Vec<SemNode>,
452 pub focus_chain: Vec<u64>,
453}
454
455#[derive(Clone, Default)]
456pub struct HitRegion {
457 pub id: u64,
458 pub rect: Rect,
459 pub depth: u32,
462 pub parent: Option<u64>,
463 pub on_click: Option<Rc<dyn Fn()>>,
464 pub on_double_click: Option<Rc<dyn Fn()>>,
465 pub on_long_click: Option<Rc<dyn Fn()>>,
466 pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
467 pub focusable: bool,
468 pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
469 pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
470 pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
471 pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
472 pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
473 pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
474 pub z_index: f32,
475 pub disabled: bool,
476 pub on_text_change: Option<Rc<dyn Fn(String)>>,
477 pub on_text_submit: Option<Rc<dyn Fn(String)>>,
478 pub tf_state_key: Option<u64>,
481
482 pub tf_multiline: bool,
484
485 pub tf_content_origin: Option<(f32, f32)>,
490
491 pub tf_enabled: bool,
493
494 pub tf_read_only: bool,
496
497 pub tf_value: String,
499
500 pub tf_font_size_dp: f32,
503
504 pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
506 pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
507 pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
508 pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
509 pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
510 pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
511 pub drag_preview: Option<crate::dnd::DragPreview>,
513
514 pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
515
516 pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
519 pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
521
522 pub cursor: Option<crate::CursorIcon>,
524
525 pub focus_group_id: Option<u64>,
530
531 pub keyboard_type: crate::text::KeyboardType,
534 pub capitalization: crate::text::KeyboardCapitalization,
535 pub ime_action: crate::text::ImeAction,
536 pub auto_correct: Option<bool>,
539
540 pub interaction_source: Option<crate::modifier::InteractionSource>,
545}
546
547impl HitRegion {
548 pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
552 Self {
553 id,
554 rect,
555 z_index: m.z_index,
556 on_click: m.on_click.clone(),
557 on_double_click: m.on_double_click.clone(),
558 on_long_click: m.on_long_click.clone(),
559 on_pointer_down: m.on_pointer_down.clone(),
560 on_pointer_move: m.on_pointer_move.clone(),
561 on_pointer_up: m.on_pointer_up.clone(),
562 on_pointer_cancel: m.on_pointer_cancel.clone(),
563 on_pointer_enter: m.on_pointer_enter.clone(),
564 on_pointer_leave: m.on_pointer_leave.clone(),
565 on_action: m.on_action.clone(),
566 on_key_event: m.on_key_event.clone(),
567 on_preview_key_event: m.on_preview_key_event.clone(),
568 cursor: m.cursor,
569 on_drag_start: m.on_drag_start.clone(),
570 on_drag_end: m.on_drag_end.clone(),
571 on_drag_enter: m.on_drag_enter.clone(),
572 on_drag_over: m.on_drag_over.clone(),
573 on_drag_leave: m.on_drag_leave.clone(),
574 on_scroll: m.on_scroll.clone(),
575 on_drop: m.on_drop.clone(),
576 drag_preview: m.drag_preview.clone(),
577 disabled: m.disabled,
578 tf_enabled: true,
579 tf_read_only: false,
580 ..Default::default()
581 }
582 }
583}
584
585#[derive(Clone)]
593pub struct SemNode {
594 pub id: u64,
596
597 pub parent: Option<u64>,
599
600 pub role: Role,
601 pub label: Option<String>,
602 pub rect: Rect,
603 pub focused: bool,
604 pub enabled: bool,
605 pub selectable_group: bool,
607 pub checked: Option<bool>,
608 pub selected: Option<bool>,
609 pub value: Option<String>,
610}
611
612impl Default for SemNode {
613 fn default() -> Self {
614 Self {
615 id: 0,
616 parent: None,
617 role: Role::default(),
618 label: None,
619 rect: Rect::default(),
620 focused: false,
621 enabled: true,
622 selectable_group: false,
623 checked: None,
624 selected: None,
625 value: None,
626 }
627 }
628}
629
630pub struct Scheduler {
631 next_id: u64,
632 scope_key_to_id: FxHashMap<String, u32>,
635 next_scope_id: u32,
636 current_scope: Option<String>,
640 scope_local_counters: FxHashMap<String, u32>,
642 pub focused: Option<u64>,
643 pub size: (u32, u32),
644}
645
646impl Default for Scheduler {
647 fn default() -> Self {
648 Self::new()
649 }
650}
651
652impl Scheduler {
653 pub fn new() -> Self {
654 Self {
655 next_id: 1,
656 scope_key_to_id: FxHashMap::default(),
657 next_scope_id: 1,
658 current_scope: None,
659 scope_local_counters: FxHashMap::default(),
660 focused: None,
661 size: (1280, 800),
662 }
663 }
664
665 pub fn enter_scope(&mut self, key: &str) {
670 self.current_scope = Some(key.to_string());
671 self.scope_local_counters.insert(key.to_string(), 0);
673 self.get_or_create_scope_id(key);
675 }
676
677 pub fn exit_scope(&mut self) {
679 self.current_scope = None;
680 }
681
682 fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
683 if let Some(&id) = self.scope_key_to_id.get(key) {
684 id
685 } else {
686 let id = self.next_scope_id;
687 self.next_scope_id += 1;
688 self.scope_key_to_id.insert(key.to_string(), id);
689 id
690 }
691 }
692
693 pub fn id(&mut self) -> u64 {
694 if let Some(key) = &self.current_scope {
695 let scope_id = self.scope_key_to_id.get(key).copied().unwrap_or(0);
697 let local = self.scope_local_counters.get_mut(key).unwrap();
698 let id = *local;
699 *local += 1;
700 (scope_id as u64) << 32 | id as u64
701 } else {
702 let id = self.next_id;
704 self.next_id += 1;
705 id
706 }
707 }
708
709 pub fn id_count(&self) -> u64 {
710 self.next_id - 1
711 }
712
713 pub fn snapshot_id(&self) -> u64 {
716 self.next_id
717 }
718
719 pub fn advance_id(&mut self, count: u32) {
722 self.next_id += count as u64;
723 }
724
725 pub fn ids_used_since(&self, prev_id: u64) -> u32 {
728 (self.next_id - prev_id) as u32
729 }
730
731 pub fn repose<F>(
732 &mut self,
733 mut build_root: F,
734 layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
735 ) -> Frame
736 where
737 F: FnMut(&mut Scheduler) -> View,
738 {
739 let guard = ComposeGuard::begin();
740 let root = guard.scope.run(|| build_root(self));
741 let (scene, hits, sem) = layout_paint(&root, self.size);
742
743 let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
744
745 Frame {
746 scene,
747 hit_regions: hits,
748 semantics_nodes: sem,
749 focus_chain,
750 }
751 }
752}
753
754#[cfg(test)]
756pub fn clear_composer() {
757 COMPOSER.with(|c| {
758 let mut c = c.borrow_mut();
759 c.slots.clear();
760 c.slot_callers.clear();
761 c.keyed_slots.clear();
762 c.scope_caches.clear();
763 c.cursor = 0;
764 });
765 ROOT_SCOPE.with(|rs| {
766 *rs.borrow_mut() = None;
767 });
768}