Skip to main content

repose_core/
runtime.rs

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, FxHashSet};
9
10use crate::effects::Dispose;
11use crate::scope::Scope;
12use crate::{CursorIcon, Rect, Scene, View, input::PhysicalKey, semantics::Role};
13
14thread_local! {
15    pub static COMPOSER: RefCell<Composer> = RefCell::new(Composer::default());
16    static ROOT_SCOPE: RefCell<Option<Scope>> = const { RefCell::new(None) };
17    static INITIALIZER_STACK: RefCell<Vec<InitializerKind>> =
18        const { RefCell::new(Vec::new()) };
19    static PENDING_INITIALIZER_CURSOR: RefCell<Option<usize>> =
20        const { RefCell::new(None) };
21    static KEYED_DISPOSERS: RefCell<FxHashMap<String, KeyedDisposerEntry>> =
22        RefCell::new(FxHashMap::default());
23    static KEYED_DISPOSER_ORDER: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
24    static PENDING_KEYED_REGISTRATIONS: RefCell<Vec<(String, Dispose, String)>> =
25        const { RefCell::new(Vec::new()) };
26
27    /// Programmatic focus requests queued by `FocusRequester`. A queue (not a
28    /// single slot) so multiple requests in one frame are honored in order
29    /// instead of last-wins. `CLEAR_FOCUS_MARKER` entries clear focus.
30    static FOCUS_REQUESTS: RefCell<std::collections::VecDeque<u64>> =
31        const { RefCell::new(std::collections::VecDeque::new()) };
32
33    /// Captured-focus lock (`FocusRequester::capture_focus`). While set,
34    /// queued requests for other targets are denied and focus stays pinned.
35    static FOCUS_CAPTURED: RefCell<Option<u64>> = const { RefCell::new(None) };
36}
37
38pub const CLEAR_FOCUS_MARKER: u64 = u64::MAX;
39
40const ROOT_OWNER: &str = "\0repose:root-owner";
41
42struct KeyedDisposerEntry {
43    disposer: Dispose,
44    owners: FxHashSet<String>,
45}
46
47enum InitializerKind {
48    Sequential,
49    Keyed,
50}
51
52pub(crate) fn scope_owner_token(scope: Option<&str>) -> String {
53    match scope {
54        Some(scope) if !scope.is_empty() => scope.to_string(),
55        _ => ROOT_OWNER.to_string(),
56    }
57}
58
59fn queue_keyed_registration(key: String, disposer: Dispose, owner: String) {
60    let queued = PENDING_KEYED_REGISTRATIONS.try_with(|pending| {
61        pending.try_borrow_mut().ok().map(|mut pending| {
62            pending.push((key, disposer, owner));
63            true
64        })
65    });
66    if !matches!(queued, Ok(Some(true))) {
67        crate::request_frame();
68    }
69}
70
71pub(crate) fn flush_keyed_disposer_registrations() {
72    let pending = PENDING_KEYED_REGISTRATIONS.try_with(|pending| {
73        pending
74            .try_borrow_mut()
75            .ok()
76            .map(|mut pending| std::mem::take(&mut *pending))
77    });
78    let Ok(Some(pending)) = pending else {
79        crate::request_frame();
80        return;
81    };
82    for (key, disposer, owner) in pending {
83        register_keyed_disposer_for_owner(key, disposer, owner);
84    }
85}
86
87pub(crate) fn register_keyed_disposer(key: String, disposer: Dispose) {
88    let owner = scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
89    register_keyed_disposer_for_owner(key, disposer, owner);
90}
91
92pub(crate) fn register_keyed_disposer_for_owner(key: String, disposer: Dispose, owner: String) {
93    let result = KEYED_DISPOSERS
94        .try_with(|registry| {
95            let mut registry = match registry.try_borrow_mut() {
96                Ok(registry) => registry,
97                Err(_) => return Err(()),
98            };
99            if let Some(entry) = registry.get_mut(&key) {
100                if entry.owners.contains(&owner) && !entry.disposer.ptr_eq(&disposer) {
101                    let previous = std::mem::replace(&mut entry.disposer, disposer.clone());
102                    return Ok((false, Some(previous)));
103                }
104                entry.owners.insert(owner.clone());
105                return Ok((false, None));
106            }
107            registry.insert(
108                key.clone(),
109                KeyedDisposerEntry {
110                    disposer: disposer.clone(),
111                    owners: FxHashSet::from_iter([owner.clone()]),
112                },
113            );
114            Ok((true, None))
115        })
116        .ok();
117    match result {
118        Some(Ok((true, previous))) => {
119            let _ = KEYED_DISPOSER_ORDER.try_with(|order| {
120                if let Ok(mut order) = order.try_borrow_mut()
121                    && !order.iter().any(|registered| registered == &key)
122                {
123                    order.push(key.clone());
124                }
125            });
126            drop(previous);
127        }
128        Some(Ok((false, previous))) => {
129            if let Some(previous) = previous {
130                run_keyed_disposer(previous);
131            }
132        }
133        Some(Err(())) | None => queue_keyed_registration(key, disposer, owner),
134    }
135}
136
137pub(crate) fn keyed_disposer_has_owner(key: &str, disposer: &Dispose, owner: &str) -> bool {
138    KEYED_DISPOSERS
139        .try_with(|registry| {
140            registry.try_borrow().ok().and_then(|registry| {
141                registry
142                    .get(key)
143                    .filter(|entry| entry.disposer.ptr_eq(disposer))
144                    .map(|entry| entry.owners.contains(owner))
145            })
146        })
147        .ok()
148        .flatten()
149        .unwrap_or(false)
150}
151
152pub(crate) fn remove_keyed_disposer_for_owner(key: &str, disposer: &Dispose, owner: &str) -> bool {
153    let mut to_run = None;
154    let mut owner_removed = false;
155    let mut registry_removed = false;
156    let result = KEYED_DISPOSERS.try_with(|registry| {
157        let mut registry = match registry.try_borrow_mut() {
158            Ok(registry) => registry,
159            Err(_) => return false,
160        };
161        let Some(entry) = registry.get_mut(key) else {
162            return true;
163        };
164        if !entry.disposer.ptr_eq(disposer) {
165            return true;
166        }
167        owner_removed = entry.owners.remove(owner);
168        if entry.owners.is_empty()
169            && let Some(entry) = registry.remove(key)
170        {
171            to_run = Some(entry.disposer);
172            registry_removed = true;
173        }
174        true
175    });
176    if !matches!(result, Ok(true)) {
177        queue_keyed_registration(key.to_string(), disposer.clone(), owner.to_string());
178        return false;
179    }
180    if registry_removed {
181        let _ = KEYED_DISPOSER_ORDER.try_with(|order| {
182            if let Ok(mut order) = order.try_borrow_mut() {
183                order.retain(|registered| registered != key);
184            }
185        });
186    }
187    if let Some(disposer) = to_run {
188        run_keyed_disposer(disposer);
189        true
190    } else {
191        owner_removed
192    }
193}
194
195pub(crate) fn take_keyed_disposer(key: &str) -> Option<Dispose> {
196    let disposer = KEYED_DISPOSERS
197        .try_with(|registry| {
198            registry
199                .try_borrow_mut()
200                .ok()?
201                .remove(key)
202                .map(|entry| entry.disposer)
203        })
204        .ok()
205        .flatten();
206    if disposer.is_some() {
207        let _ = KEYED_DISPOSER_ORDER.try_with(|order| {
208            if let Ok(mut order) = order.try_borrow_mut() {
209                order.retain(|registered| registered != key);
210            }
211        });
212    }
213    disposer
214}
215
216pub(crate) fn take_all_keyed_disposers() -> Vec<Dispose> {
217    flush_keyed_disposer_registrations();
218    let order = KEYED_DISPOSER_ORDER
219        .try_with(|order| {
220            order
221                .try_borrow_mut()
222                .ok()
223                .map(|mut order| std::mem::take(&mut *order))
224        })
225        .ok()
226        .flatten()
227        .unwrap_or_default();
228
229    KEYED_DISPOSERS
230        .try_with(|registry| {
231            registry.try_borrow_mut().ok().map(|mut registry| {
232                let mut result = Vec::with_capacity(registry.len());
233                for key in order.into_iter().rev() {
234                    if let Some(entry) = registry.remove(&key) {
235                        result.push(entry.disposer);
236                    }
237                }
238                result.extend(registry.drain().map(|(_, entry)| entry.disposer));
239                result
240            })
241        })
242        .ok()
243        .flatten()
244        .unwrap_or_default()
245}
246
247pub(crate) fn run_keyed_disposer(disposer: Dispose) {
248    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| disposer.run()));
249}
250
251/// Process-wide unique id for namespacing `remember_with_key` slots
252/// (dialogs, menus, sheets, tooltips, ...).
253static COMPONENT_ID: AtomicU64 = AtomicU64::new(1);
254
255/// Returns a fresh unique component id. See [`COMPONENT_ID`].
256pub fn unique_component_id() -> u64 {
257    COMPONENT_ID.fetch_add(1, Ordering::Relaxed)
258}
259
260pub fn take_focus_request() -> Option<u64> {
261    FOCUS_REQUESTS.with(|r| r.borrow_mut().pop_front())
262}
263
264/// Drain all queued focus requests in FIFO order.
265///
266/// While a capture lock is held, requests for other targets are denied and
267/// the captured id is returned instead, pinning focus.
268pub fn drain_focus_requests() -> Vec<u64> {
269    if let Some(captured) = FOCUS_CAPTURED.with(|c| *c.borrow()) {
270        FOCUS_REQUESTS.with(|r| r.borrow_mut().clear());
271        return vec![captured];
272    }
273    FOCUS_REQUESTS.with(|r| r.borrow_mut().drain(..).collect())
274}
275
276/// Currently captured focus target, if any.
277pub fn focus_capture() -> Option<u64> {
278    FOCUS_CAPTURED.with(|c| *c.borrow())
279}
280
281/// A handle that can programmatically request focus for a widget.
282///
283/// Similar to Compose's `FocusRequester`. Create one via `remember(FocusRequester::new)`,
284/// attach it via `.focus_requester(...)` on a modifier, and call `request_focus()` to
285/// move keyboard focus to the associated widget on the next frame.
286#[derive(Clone)]
287pub struct FocusRequester {
288    /// Target view ID, set during layout/paint by the modifier system.
289    pub target: Rc<RefCell<Option<u64>>>,
290}
291
292impl FocusRequester {
293    pub fn new() -> Self {
294        Self {
295            target: Rc::new(RefCell::new(None)),
296        }
297    }
298
299    /// Request focus for the associated widget on the next frame.
300    /// Queued; unlike the old single-slot behavior, multiple requests in one
301    /// frame are all honored in order. If the target is not laid out yet the
302    /// request is dropped (target assigned during layout/paint).
303    pub fn request_focus(&self) {
304        let _ = self.try_request_focus();
305    }
306
307    /// Enqueue a focus request, reporting whether it was accepted.
308    /// Returns `false` when the target is not laid out yet (request dropped)
309    /// or when another target holds the capture lock.
310    pub fn try_request_focus(&self) -> bool {
311        let Some(id) = *self.target.borrow() else {
312            return false;
313        };
314        if let Some(captured) = FOCUS_CAPTURED.with(|c| *c.borrow())
315            && captured != id
316        {
317            return false;
318        }
319        FOCUS_REQUESTS.with(|r| r.borrow_mut().push_back(id));
320        true
321    }
322
323    /// Request focus for the associated widget from a traversal direction.
324    /// Directional enter delegation is not implemented (focus targets are
325    /// flat, not containers), so this behaves like [`try_request_focus`](Self::try_request_focus)
326    /// and reports acceptance the same way.
327    pub fn request_focus_directed(&self, _direction: FocusDirection) -> bool {
328        self.try_request_focus()
329    }
330
331    /// Free/clear focus from the associated widget on the next frame.
332    /// If the associated widget currently has focus, focus is cleared entirely.
333    /// Corresponds to Compose's `freeFocus()`. Releases this target's capture
334    /// lock when held.
335    pub fn free_focus(&self) {
336        if let Some(id) = *self.target.borrow() {
337            FOCUS_CAPTURED.with(|c| {
338                if *c.borrow() == Some(id) {
339                    *c.borrow_mut() = None;
340                }
341            });
342        }
343        FOCUS_REQUESTS.with(|r| r.borrow_mut().push_back(CLEAR_FOCUS_MARKER));
344    }
345
346    /// Capture focus for the associated widget: focus moves to the target and
347    /// stays pinned there until [`free_focus`](Self::free_focus). Other
348    /// targets' requests are denied while the lock is held.
349    /// Returns `false` when the target is not laid out yet.
350    pub fn capture_focus(&self) -> bool {
351        let Some(id) = *self.target.borrow() else {
352            return false;
353        };
354        FOCUS_CAPTURED.with(|c| *c.borrow_mut() = Some(id));
355        FOCUS_REQUESTS.with(|r| r.borrow_mut().push_back(id));
356        true
357    }
358}
359
360impl Default for FocusRequester {
361    fn default() -> Self {
362        Self::new()
363    }
364}
365
366/// Direction for focus movement.
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
368pub enum FocusDirection {
369    Next,
370    Previous,
371    Left,
372    Right,
373    Up,
374    Down,
375}
376
377/// A manager for programmatic focus navigation.
378///
379/// Wraps a `&Scheduler` and provides methods to move focus.
380/// Can also be used standalone with a focus chain and focused element.
381#[derive(Clone)]
382pub struct FocusManager {
383    /// The ordered list of focusable element IDs.
384    pub chain: Vec<u64>,
385    /// The currently focused element (if any).
386    pub focused: Option<u64>,
387    /// Hit regions for focus group lookups.
388    pub hit_regions: Vec<HitRegion>,
389}
390
391impl FocusManager {
392    pub fn new(chain: Vec<u64>, focused: Option<u64>) -> Self {
393        Self {
394            chain,
395            focused,
396            hit_regions: Vec::new(),
397        }
398    }
399
400    /// Move focus in the given direction.
401    /// Returns the new focused element ID, or `None` if no movement is possible.
402    pub fn move_focus(&mut self, dir: FocusDirection) -> Option<u64> {
403        match dir {
404            FocusDirection::Next | FocusDirection::Previous => {
405                self.move_tab(dir == FocusDirection::Previous)
406            }
407            _ => None, // use move_focus_spatial when hit regions are available
408        }
409    }
410
411    /// Clears focus entirely on the next frame.
412    /// Corresponds to Compose's `FocusManager.clearFocus()`.
413    /// The `force` parameter is accepted for API compatibility. In repose
414    /// focus is always cleared immediately (no keep-focus mechanism).
415    pub fn clear_focus(&self, _force: bool) {
416        FOCUS_REQUESTS.with(|r| r.borrow_mut().push_back(CLEAR_FOCUS_MARKER));
417    }
418
419    /// Spatial focus navigation: find the closest focusable element in a given
420    /// direction using bounding rect geometry.
421    pub fn move_focus_spatial(
422        &mut self,
423        dir: FocusDirection,
424        hit_regions: &[HitRegion],
425    ) -> Option<u64> {
426        let next = spatial_focus_next(&self.chain, hit_regions, self.focused, dir)?;
427        self.focused = Some(next);
428        Some(next)
429    }
430
431    /// Tab forward or backward in the focus chain.
432    /// When the current focus belongs to a focus group, navigation is restricted
433    /// to elements within that group.
434    pub fn move_tab(&mut self, reverse: bool) -> Option<u64> {
435        if self.chain.is_empty() {
436            return None;
437        }
438        let next = if let Some(sub_chain) =
439            focus_group_chain(&self.chain, &self.hit_regions, self.focused)
440        {
441            if sub_chain.is_empty() {
442                return None;
443            }
444            if let Some(cur) = self.focused {
445                if let Some(idx) = sub_chain.iter().position(|&id| id == cur) {
446                    if reverse {
447                        if idx == 0 {
448                            sub_chain[sub_chain.len() - 1]
449                        } else {
450                            sub_chain[idx - 1]
451                        }
452                    } else {
453                        sub_chain[(idx + 1) % sub_chain.len()]
454                    }
455                } else {
456                    sub_chain[0]
457                }
458            } else if reverse {
459                sub_chain[sub_chain.len() - 1]
460            } else {
461                sub_chain[0]
462            }
463        } else {
464            if let Some(cur) = self.focused {
465                if let Some(idx) = self.chain.iter().position(|&id| id == cur) {
466                    if reverse {
467                        if idx == 0 {
468                            self.chain[self.chain.len() - 1]
469                        } else {
470                            self.chain[idx - 1]
471                        }
472                    } else {
473                        self.chain[(idx + 1) % self.chain.len()]
474                    }
475                } else {
476                    self.chain[0]
477                }
478            } else if reverse {
479                self.chain[self.chain.len() - 1]
480            } else {
481                self.chain[0]
482            }
483        };
484        self.focused = Some(next);
485        Some(next)
486    }
487
488    /// Set target ID on a FocusRequester (called during layout).
489    pub fn set_requester_target(requester: &FocusRequester, id: u64) {
490        *requester.target.borrow_mut() = Some(id);
491    }
492}
493
494/// Sub-chain of ids sharing the focused element's focus group.
495/// Returns `None` when focus is outside any group (full chain applies).
496/// Tab and spatial navigation both scope to this so modals trap focus.
497pub fn focus_group_chain(
498    chain: &[u64],
499    hit_regions: &[HitRegion],
500    current: Option<u64>,
501) -> Option<Vec<u64>> {
502    let cur = current?;
503    let group_id = hit_regions.iter().find(|h| h.id == cur)?.focus_group_id?;
504    Some(
505        chain
506            .iter()
507            .copied()
508            .filter(|&id| {
509                id == group_id
510                    || hit_regions
511                        .iter()
512                        .any(|h| h.id == id && h.focus_group_id == Some(group_id))
513            })
514            .collect(),
515    )
516}
517
518/// Find the next focusable element in a given spatial direction.
519///
520/// Uses the bounding rects from `hit_regions` to determine which element is
521/// "next" in the given direction from the currently focused element.
522pub fn spatial_focus_next(
523    chain: &[u64],
524    hit_regions: &[HitRegion],
525    current: Option<u64>,
526    dir: FocusDirection,
527) -> Option<u64> {
528    if chain.is_empty() {
529        return None;
530    }
531
532    let current_rect =
533        current.and_then(|id| hit_regions.iter().find(|h| h.id == id).map(|h| h.rect));
534
535    // For Next/Previous, use tab-order navigation
536    match dir {
537        FocusDirection::Next | FocusDirection::Previous => {
538            let mut fm = FocusManager {
539                chain: chain.to_vec(),
540                focused: current,
541                hit_regions: hit_regions.to_vec(),
542            };
543            return fm.move_tab(dir == FocusDirection::Previous);
544        }
545        _ => {}
546    }
547
548    let (cx, cy) = match current_rect {
549        Some(r) => (r.x + r.w / 2.0, r.y + r.h / 2.0),
550        None => {
551            // For games: Tab/Shift-Tab should establish initial focus, not arrows.
552            return None;
553        }
554    };
555
556    let mut best: Option<(u64, f32)> = None;
557
558    // Modal trap: arrows stay inside the focused element's focus group.
559    let scoped: Vec<u64>;
560    let chain: &[u64] = match focus_group_chain(chain, hit_regions, current) {
561        Some(sub) => {
562            scoped = sub;
563            &scoped
564        }
565        None => chain,
566    };
567
568    for &id in chain {
569        if Some(id) == current {
570            continue;
571        }
572        let Some(hr) = hit_regions.iter().find(|h| h.id == id) else {
573            continue;
574        };
575        let r = hr.rect;
576        let other_cx = r.x + r.w / 2.0;
577        let other_cy = r.y + r.h / 2.0;
578        let dx = other_cx - cx;
579        let dy = other_cy - cy;
580
581        let in_direction = match dir {
582            FocusDirection::Left => dx < 0.0 && dy.abs() <= r.h.max(1.0),
583            FocusDirection::Right => dx > 0.0 && dy.abs() <= r.h.max(1.0),
584            FocusDirection::Up => dy < 0.0 && dx.abs() <= r.w.max(1.0),
585            FocusDirection::Down => dy > 0.0 && dx.abs() <= r.w.max(1.0),
586            _ => false,
587        };
588
589        if !in_direction {
590            continue;
591        }
592
593        let dist = dx * dx + dy * dy;
594        let weight = dist / (r.w * r.h + 1.0).max(1.0);
595
596        match best {
597            Some((_, best_weight)) if weight >= best_weight => {}
598            _ => best = Some((id, weight)),
599        }
600    }
601
602    best.map(|(id, _)| id)
603}
604
605#[derive(Default)]
606pub struct Composer {
607    pub slots: Vec<Box<dyn Any>>,
608    pub slot_callers: Vec<&'static Location<'static>>,
609    pub cursor: usize,
610    pub auto_counts: FxHashMap<String, usize>,
611    pub keyed_slots: FxHashMap<String, Box<dyn Any>>,
612    pub keyed_owner: FxHashMap<String, String>,
613    pub keyed_owners: FxHashMap<String, rustc_hash::FxHashSet<String>>,
614    /// Owner tokens of registered scopes visited this frame, plus the root token.
615    pub live_keyed_owners: rustc_hash::FxHashSet<String>,
616    /// Raw keys read through `remember_with_key` this frame.
617    pub live_keyed_keys: rustc_hash::FxHashSet<String>,
618    /// Owner tokens of scopes served from cache this frame. Their bodies did not
619    /// re-run, so their keyed slots go unread and must not be collected.
620    pub cache_preserved_owners: rustc_hash::FxHashSet<String>,
621    pub scope_caches: FxHashMap<String, crate::scope_cache::ScopeCache>,
622    pub live_scope_keys: rustc_hash::FxHashSet<String>,
623}
624
625pub struct ComposeGuard {
626    scope: Scope,
627}
628
629pub(crate) fn keyed_disposer_order(key: &str) -> usize {
630    KEYED_DISPOSER_ORDER
631        .try_with(|order| {
632            order
633                .try_borrow()
634                .ok()
635                .and_then(|order| order.iter().position(|registered| registered == key))
636        })
637        .ok()
638        .flatten()
639        .unwrap_or(usize::MAX)
640}
641
642pub(crate) fn release_keyed_owner(key: &str, owner: &str) -> (bool, Option<Box<dyn Any>>) {
643    let result = COMPOSER
644        .try_with(|composer| {
645            let mut composer = match composer.try_borrow_mut() {
646                Ok(composer) => composer,
647                Err(_) => return (false, None, None),
648            };
649            let owners = match composer.keyed_owners.get_mut(key) {
650                Some(owners) => owners,
651                None => return (false, None, None),
652            };
653            owners.remove(owner);
654            if !owners.is_empty() {
655                return (true, None, None);
656            }
657            composer.keyed_owners.remove(key);
658            let old_owner = composer.keyed_owner.remove(key);
659            (true, composer.keyed_slots.remove(key), old_owner)
660        })
661        .unwrap_or((false, None, None));
662    drop(result.2);
663    (result.0, result.1)
664}
665
666/// A keyed slot survives when both hold:
667///
668/// * an owning scope is live (registered and visited this frame, or root) —
669///   unchanged rule, so slots whose owner is not a real scope (a bare
670///   `with_scope_key`, as `produce_state` tests use) are still collected, and
671///   `ProduceHandle::drop` can strip the reactive observer; and
672/// * the key was read this frame, or an owning scope was served from cache and
673///   so legitimately never read it.
674///
675/// The second condition is the fix. A scope that re-runs but stops reading a
676/// key now drops it, so a slot abandoned inside a permanently mounted scope —
677/// an overlay guard held by a component the host stopped composing, releases
678/// its entry instead of pinning it for the life of the app.
679pub(crate) fn take_dead_keyed_slots(composer: &mut Composer) -> Vec<(String, Box<dyn Any>)> {
680    fn owners_of<'a>(composer: &'a Composer, key: &str) -> Vec<&'a String> {
681        match composer.keyed_owners.get(key) {
682            Some(owners) => owners.iter().collect(),
683            None => composer.keyed_owner.get(key).into_iter().collect(),
684        }
685    }
686    let dead: Vec<String> = composer
687        .keyed_slots
688        .keys()
689        .filter(|key| {
690            let owners = owners_of(composer, key);
691            let scope_live = owners
692                .iter()
693                .any(|owner| composer.live_keyed_owners.contains(*owner));
694            if !scope_live {
695                return true;
696            }
697            if composer.live_keyed_keys.contains(*key) {
698                return false;
699            }
700            !owners
701                .iter()
702                .any(|owner| composer.cache_preserved_owners.contains(*owner))
703        })
704        .cloned()
705        .collect();
706    let mut removed = Vec::with_capacity(dead.len());
707    for key in dead {
708        if let Some(value) = composer.keyed_slots.remove(&key) {
709            removed.push((key.clone(), value));
710        }
711        composer.keyed_owner.remove(&key);
712        composer.keyed_owners.remove(&key);
713    }
714    removed
715}
716
717pub(crate) fn current_scope_key_for_remember() -> Option<String> {
718    crate::scope_cache::current_scope_key()
719}
720
721fn flush_initializer_cursor() {
722    let pending = PENDING_INITIALIZER_CURSOR
723        .try_with(|pending| {
724            pending
725                .try_borrow_mut()
726                .ok()
727                .and_then(|mut pending| pending.take())
728        })
729        .ok()
730        .flatten();
731    let Some(cursor) = pending else {
732        return;
733    };
734    let restored = COMPOSER
735        .try_with(|composer| {
736            composer
737                .try_borrow_mut()
738                .ok()
739                .map(|mut composer| composer.cursor = cursor)
740        })
741        .ok()
742        .flatten();
743    if restored.is_none() {
744        let _ = PENDING_INITIALIZER_CURSOR.try_with(|pending| {
745            if let Ok(mut pending) = pending.try_borrow_mut() {
746                *pending = Some(cursor);
747            }
748        });
749    }
750}
751
752impl ComposeGuard {
753    pub fn begin() -> Self {
754        flush_initializer_cursor();
755        COMPOSER.with(|c| {
756            let mut c = c.borrow_mut();
757            c.cursor = 0;
758            c.auto_counts.clear();
759            c.live_scope_keys.clear();
760            c.live_keyed_owners.clear();
761            c.live_keyed_keys.clear();
762            c.cache_preserved_owners.clear();
763            c.live_scope_keys.insert(String::new());
764            c.live_keyed_owners.insert(scope_owner_token(None));
765        });
766
767        let scope = ROOT_SCOPE.with(|rs| {
768            if let Some(existing) = rs.borrow().clone() {
769                existing
770            } else {
771                let s = Scope::new();
772                *rs.borrow_mut() = Some(s.clone());
773                s
774            }
775        });
776
777        ComposeGuard { scope }
778    }
779
780    pub fn scope(&self) -> &Scope {
781        &self.scope
782    }
783
784    pub fn live_scope_keys(&self) -> rustc_hash::FxHashSet<String> {
785        COMPOSER.with(|c| c.borrow().live_scope_keys.clone())
786    }
787}
788
789impl Drop for ComposeGuard {
790    fn drop(&mut self) {
791        let removed = COMPOSER.with(|c| {
792            let mut c = c.borrow_mut();
793            let n = c.cursor;
794            let mut removed = Vec::new();
795            if c.slots.len() > n {
796                removed.extend(c.slots.drain(n..));
797            }
798            if c.slot_callers.len() > n {
799                c.slot_callers.truncate(n);
800            }
801            removed
802        });
803        drop(removed);
804        crate::scope_cache::gc_dead_scopes();
805    }
806}
807
808/// Dispose the root composition scope and clear all composer caches.
809///
810/// Call once on process exit (desktop `exiting`, tests). After this the next
811/// `ComposeGuard::begin` starts from a fresh root scope.
812pub fn shutdown_composition() {
813    let scope = ROOT_SCOPE.with(|rs| rs.borrow_mut().take());
814    if let Some(scope) = scope {
815        scope.dispose();
816    }
817    let removed = COMPOSER.with(|c| {
818        let mut c = c.borrow_mut();
819        let slots = std::mem::take(&mut c.slots);
820        let callers = std::mem::take(&mut c.slot_callers);
821        let keyed = std::mem::take(&mut c.keyed_slots);
822        let caches = std::mem::take(&mut c.scope_caches);
823        c.keyed_owner.clear();
824        c.keyed_owners.clear();
825        c.live_keyed_owners.clear();
826        c.live_keyed_keys.clear();
827        c.cache_preserved_owners.clear();
828        c.live_scope_keys.clear();
829        c.cursor = 0;
830        c.auto_counts.clear();
831        (slots, callers, keyed, caches)
832    });
833    drop(removed);
834    for disposer in take_all_keyed_disposers() {
835        run_keyed_disposer(disposer);
836    }
837    crate::animation_driver::shutdown();
838    crate::scope_cache::clear_all_scope_deps();
839    INITIALIZER_STACK.with(|stack| stack.borrow_mut().clear());
840    PENDING_INITIALIZER_CURSOR.with(|pending| {
841        let _ = pending.borrow_mut().take();
842    });
843}
844
845struct InitializerGuard {
846    active: bool,
847    cursor: Option<usize>,
848}
849
850impl InitializerGuard {
851    fn assert_sequential_allowed() {
852        assert!(
853            INITIALIZER_STACK.with(|stack| stack
854                .try_borrow()
855                .map(|stack| stack.is_empty())
856                .unwrap_or(false)),
857            "remember initializer re-entered composition; use an explicit keyed helper"
858        );
859    }
860
861    fn enter_sequential() -> (Self, usize) {
862        Self::assert_sequential_allowed();
863        let cursor = COMPOSER.with(|composer| {
864            let mut composer = composer.borrow_mut();
865            let cursor = composer.cursor;
866            composer.cursor = composer.cursor.wrapping_add(1);
867            cursor
868        });
869        let pushed = INITIALIZER_STACK
870            .try_with(|stack| {
871                stack.try_borrow_mut().ok().map(|mut stack| {
872                    stack.push(InitializerKind::Sequential);
873                    true
874                })
875            })
876            .ok()
877            .flatten();
878        if pushed != Some(true) {
879            let _ = COMPOSER.try_with(|composer| {
880                if let Ok(mut composer) = composer.try_borrow_mut() {
881                    composer.cursor = cursor;
882                }
883            });
884            panic!("initializer stack busy");
885        }
886        (
887            Self {
888                active: true,
889                cursor: Some(cursor),
890            },
891            cursor,
892        )
893    }
894
895    fn assert_keyed_allowed() {
896        assert!(
897            INITIALIZER_STACK.with(|stack| stack
898                .try_borrow()
899                .map(|stack| stack.is_empty())
900                .unwrap_or(false)),
901            "remember initializer re-entered composition; use an explicit keyed helper"
902        );
903    }
904
905    fn enter_keyed() -> Self {
906        Self::assert_keyed_allowed();
907        INITIALIZER_STACK.with(|stack| {
908            stack
909                .try_borrow_mut()
910                .expect("initializer stack busy")
911                .push(InitializerKind::Keyed)
912        });
913        Self {
914            active: true,
915            cursor: None,
916        }
917    }
918
919    fn commit(&mut self) {
920        if !self.active {
921            return;
922        }
923        let _ = INITIALIZER_STACK.try_with(|stack| {
924            if let Ok(mut stack) = stack.try_borrow_mut() {
925                stack.pop();
926            }
927        });
928        self.active = false;
929    }
930}
931
932impl Drop for InitializerGuard {
933    fn drop(&mut self) {
934        if !self.active {
935            return;
936        }
937        if let Some(cursor) = self.cursor {
938            let restored = COMPOSER
939                .try_with(|composer| {
940                    composer
941                        .try_borrow_mut()
942                        .ok()
943                        .map(|mut composer| composer.cursor = cursor)
944                })
945                .ok()
946                .flatten();
947            if restored.is_none() {
948                let _ = PENDING_INITIALIZER_CURSOR.try_with(|pending| {
949                    if let Ok(mut pending) = pending.try_borrow_mut() {
950                        *pending = Some(cursor);
951                    }
952                });
953            }
954        }
955        let _ = INITIALIZER_STACK.try_with(|stack| {
956            if let Ok(mut stack) = stack.try_borrow_mut() {
957                stack.pop();
958            }
959        });
960    }
961}
962
963/// Slot-based remember (sequential composition only).
964/// This prevents state aliasing when the composition tree structure changes between frames
965#[track_caller]
966pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
967    flush_initializer_cursor();
968    let caller = Location::caller();
969    let (mut initializer, cursor) = InitializerGuard::enter_sequential();
970
971    let existing = COMPOSER.with(|c| {
972        let c = c.borrow();
973        if cursor >= c.slots.len() || c.slot_callers.get(cursor).copied() != Some(caller) {
974            return None;
975        }
976        c.slots[cursor].downcast_ref::<Rc<T>>().cloned()
977    });
978    if let Some(rc) = existing {
979        initializer.commit();
980        return rc;
981    }
982
983    if COMPOSER.with(|c| {
984        let c = c.borrow();
985        cursor < c.slots.len() && c.slot_callers.get(cursor).copied() == Some(caller)
986    }) {
987        log::warn!(
988            "remember: slot {} type changed {}. \
989             Use remember_with_key(key, || ...) for conditional branches.",
990            cursor,
991            std::any::type_name::<T>(),
992        );
993    }
994
995    let rc = Rc::new(init());
996    let old = COMPOSER.with(|c| {
997        let mut c = c.borrow_mut();
998        let value: Box<dyn Any> = Box::new(rc.clone());
999        let old = if cursor < c.slots.len() {
1000            Some(std::mem::replace(&mut c.slots[cursor], value))
1001        } else {
1002            c.slots.push(value);
1003            None
1004        };
1005        if cursor < c.slot_callers.len() {
1006            c.slot_callers[cursor] = caller;
1007        } else {
1008            c.slot_callers.push(caller);
1009        }
1010        old
1011    });
1012    initializer.commit();
1013    drop(old);
1014    rc
1015}
1016
1017fn push_key_part(key: &mut String, part: &str) {
1018    key.push_str(&part.len().to_string());
1019    key.push(':');
1020    key.push_str(part);
1021    key.push('|');
1022}
1023
1024/// Auto-keyed variant of [`remember`]: identity is the call site plus an
1025/// occurrence index, so state follows a composition *position*, not an item.
1026/// Only safe where the number of calls at this site is fixed across frames —
1027/// list items and conditional branches need [`remember_with_key`].
1028#[track_caller]
1029pub fn remember_auto<T: 'static>(slot: &str, init: impl FnOnce() -> T) -> Rc<T> {
1030    let caller = Location::caller();
1031    let scope = current_scope_key_for_remember().unwrap_or_default();
1032    let mut base = String::from("auto:v1|");
1033    push_key_part(&mut base, &scope);
1034    push_key_part(&mut base, caller.file());
1035    push_key_part(&mut base, &caller.line().to_string());
1036    push_key_part(&mut base, &caller.column().to_string());
1037    push_key_part(&mut base, slot);
1038    let occurrence = COMPOSER.with(|composer| {
1039        let mut composer = composer.borrow_mut();
1040        let count = composer.auto_counts.entry(base.clone()).or_insert(0);
1041        let occurrence = *count;
1042        *count += 1;
1043        occurrence
1044    });
1045    push_key_part(&mut base, &occurrence.to_string());
1046    remember_with_key(base, init)
1047}
1048
1049pub fn keyed<T>(key: impl Into<String>, f: impl FnOnce() -> T) -> T {
1050    let key = key.into();
1051    assert!(!key.is_empty(), "keyed scopes need non-empty keys");
1052    let parent = current_scope_key_for_remember().unwrap_or_default();
1053    let mut full_key = String::new();
1054    push_key_part(&mut full_key, &parent);
1055    push_key_part(&mut full_key, &key);
1056    crate::scope_cache::with_scope_key(&full_key, f)
1057}
1058
1059/// Key-based remember.
1060pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
1061    flush_initializer_cursor();
1062    InitializerGuard::assert_keyed_allowed();
1063    let key = key.into();
1064    let scope = current_scope_key_for_remember();
1065    let owner = scope_owner_token(scope.as_deref());
1066    let existing = COMPOSER.with(|composer| {
1067        let composer = composer.borrow();
1068        composer
1069            .keyed_slots
1070            .get(&key)
1071            .and_then(|value| value.downcast_ref::<Rc<T>>().cloned())
1072    });
1073    if let Some(existing) = existing {
1074        let old_owner = COMPOSER.with(|composer| {
1075            let mut composer = composer.borrow_mut();
1076            composer.live_keyed_keys.insert(key.clone());
1077            let old_owner = composer.keyed_owner.insert(key.clone(), owner.clone());
1078            composer
1079                .keyed_owners
1080                .entry(key.clone())
1081                .or_default()
1082                .insert(owner.clone());
1083            composer.live_keyed_owners.insert(owner);
1084            old_owner
1085        });
1086        drop(old_owner);
1087        return existing;
1088    }
1089
1090    // Reaching here means the same-type early return above did not fire, so the
1091    // key is either absent or held under a different type. Keep the two signals
1092    // apart: fusing them made a type collision also log the "unbounded dynamic
1093    // keys" message, and left the real key-count check unable to fire outside
1094    // debug builds.
1095    let (wrong_type, key_count_high) = COMPOSER.with(|composer| {
1096        let composer = composer.borrow();
1097        (
1098            composer.keyed_slots.contains_key(&key),
1099            composer.keyed_slots.len() > 10_000,
1100        )
1101    });
1102    if wrong_type {
1103        log::warn!(
1104            "remember_with_key: key '{}' reused with a different type; replacing.",
1105            key
1106        );
1107    }
1108    if key_count_high {
1109        log::warn!(
1110            "remember_with_key: {} keys stored; are you generating unbounded dynamic keys?",
1111            COMPOSER.with(|composer| composer.borrow().keyed_slots.len())
1112        );
1113    }
1114
1115    let mut initializer = InitializerGuard::enter_keyed();
1116    let value = Rc::new(init());
1117    let (result, old) = COMPOSER.with(|composer| {
1118        let mut composer = composer.borrow_mut();
1119        if let Some(existing) = composer.keyed_slots.get(&key)
1120            && let Some(existing) = existing.downcast_ref::<Rc<T>>()
1121        {
1122            let existing = existing.clone();
1123            composer.live_keyed_keys.insert(key.clone());
1124            composer.keyed_owner.insert(key.clone(), owner.clone());
1125            composer
1126                .keyed_owners
1127                .entry(key.clone())
1128                .or_default()
1129                .insert(owner.clone());
1130            composer.live_keyed_owners.insert(owner);
1131            return (existing, None);
1132        }
1133        let old = composer
1134            .keyed_slots
1135            .insert(key.clone(), Box::new(value.clone()));
1136        composer.live_keyed_keys.insert(key.clone());
1137        composer.keyed_owner.insert(key.clone(), owner.clone());
1138        composer
1139            .keyed_owners
1140            .entry(key)
1141            .or_default()
1142            .insert(owner.clone());
1143        (value, old)
1144    });
1145    initializer.commit();
1146    drop(old);
1147    result
1148}
1149
1150/// Raw slot state (`Rc<RefCell<T>>`). Writes via `borrow_mut()` don't request a
1151/// frame - prefer [`remember_mutable`] if a write must always recompose.
1152#[track_caller]
1153pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
1154    remember(|| RefCell::new(init()))
1155}
1156
1157/// Key-based variant of [`remember_state`]. Same no-frame-on-write caveat.
1158pub fn remember_state_with_key<T: 'static>(
1159    key: impl Into<String>,
1160    init: impl FnOnce() -> T,
1161) -> Rc<RefCell<T>> {
1162    remember_with_key(key, || RefCell::new(init()))
1163}
1164
1165/// Auto-keyed variant of [`remember_state`]. Same positional caveat as
1166/// [`remember_auto`] and the same no-frame-on-write caveat as [`remember_state`].
1167#[track_caller]
1168pub fn remember_state_auto<T: 'static>(slot: &str, init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
1169    remember_auto(slot, || RefCell::new(init()))
1170}
1171
1172/// Frame - output of composition for a tick: scene + input/semantics.
1173#[derive(Clone)]
1174pub struct Frame {
1175    pub scene: Scene,
1176    pub hit_regions: Vec<HitRegion>,
1177    pub semantics_nodes: Vec<SemNode>,
1178    pub focus_chain: Vec<u64>,
1179}
1180
1181/// Hit-test region in physical pixels (`rect` carries px magnitudes,
1182/// like Compose `Rect`).
1183#[derive(Clone, Default)]
1184pub struct HitRegion {
1185    pub id: u64,
1186    pub rect: Rect,
1187    /// Tree depth: 0 = root, higher = deeper child. Used for three-pass
1188    /// pointer dispatch to determine ancestor/descendant ordering.
1189    pub depth: u32,
1190    pub parent: Option<u64>,
1191    pub on_click: Option<Rc<dyn Fn()>>,
1192    pub on_double_click: Option<Rc<dyn Fn()>>,
1193    pub on_long_click: Option<Rc<dyn Fn()>>,
1194    pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
1195    pub on_scroll_with_source:
1196        Option<Rc<dyn Fn(crate::Vec2, crate::input::ScrollSource) -> crate::Vec2>>,
1197    pub focusable: bool,
1198    pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1199    pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1200    pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1201    pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1202    pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1203    pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1204    pub z_index: f32,
1205    pub disabled: bool,
1206    pub on_text_change: Option<Rc<dyn Fn(String)>>,
1207    pub on_text_submit: Option<Rc<dyn Fn(String)>>,
1208    /// If this hit region belongs to a TextField, this persistent key is used
1209    /// for looking up platform-managed TextFieldState. Falls back to `id` if None.
1210    pub tf_state_key: Option<u64>,
1211
1212    /// True if this hit region corresponds to a multiline text input (TextArea).
1213    pub tf_multiline: bool,
1214
1215    /// Unclipped top-left of the TextField *content* box (padding-inset).
1216    /// Used for pointer->grapheme mapping so parent scroll clipping of `rect`
1217    /// does not shift selection into the top of the content.
1218    /// `None` for non-textfields.
1219    pub tf_content_origin: Option<(f32, f32)>,
1220
1221    /// When false, the field rejects edits and is not focusable
1222    pub tf_enabled: bool,
1223
1224    /// When true, selection/focus/copy are allowed but mutations are rejected
1225    pub tf_read_only: bool,
1226
1227    pub tf_sensitive: bool,
1228
1229    /// Controlled text snapshot for this field (last compose).
1230    pub tf_value: String,
1231
1232    /// Font size for this text field in [`Sp`](crate::units::Sp)
1233    /// (for hit-test / caret mapping). `Sp::ZERO` means use `TF_FONT_SP` default.
1234    pub tf_font_size: crate::units::Sp,
1235
1236    // internal
1237    pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
1238    pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
1239    pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1240    pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1241    pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1242    pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
1243    /// Copied onto the drag session when a drag starts from this region.
1244    pub drag_preview: Option<crate::dnd::DragPreview>,
1245
1246    pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
1247
1248    pub on_set_progress: Option<Rc<dyn Fn(f32)>>,
1249    pub on_expand: Option<Rc<dyn Fn()>>,
1250    pub on_collapse: Option<Rc<dyn Fn()>>,
1251    pub on_dismiss: Option<Rc<dyn Fn()>>,
1252
1253    /// Called when a key event is received while this element is focused.
1254    /// Return `true` to consume the event.
1255    pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
1256    /// Called before `on_key_event`. Return `true` to consume before normal dispatch.
1257    pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
1258    /// Called when an IME event (composition update/commit) is received
1259    /// while this element is focused. Return `true` to consume the event.
1260    pub on_ime: Option<Rc<dyn Fn(crate::input::ImeEvent) -> bool>>,
1261
1262    /// Cursor hint for desktop/web.
1263    pub cursor: Option<crate::CursorIcon>,
1264
1265    /// If `Some(group_id)`, this hit region belongs to a focus group with the
1266    /// given id. Tab navigation will cycle within the group instead of moving
1267    /// to elements outside it. Set automatically by the layout engine when the
1268    /// element is a descendant of a node with `focus_group: true`.
1269    pub focus_group_id: Option<u64>,
1270
1271    /// IME keyboard hints, populated for text-field hit regions so the
1272    /// platform runner can configure the OS keyboard / IME on focus.
1273    pub keyboard_type: crate::text::KeyboardType,
1274    pub capitalization: crate::text::KeyboardCapitalization,
1275    pub ime_action: crate::text::ImeAction,
1276    /// Whether auto-correct is enabled. `None` = follow platform default;
1277    /// password keyboards always resolve to `false` in the layout engine.
1278    pub auto_correct: Option<bool>,
1279
1280    /// Shared interaction source auto-wired by the layout engine
1281    /// (press/hover/focus/drag). Used by keyboard activation and focus
1282    /// transitions so they stay in parity with pointer input.
1283    /// `None` when the component does not need one (no indication/state colors).
1284    pub interaction_source: Option<crate::modifier::InteractionSource>,
1285}
1286
1287impl HitRegion {
1288    /// Seed a HitRegion with all the modifier's event handlers + dnd + cursor.
1289    /// Call‑sites should only override the fields that differ (on_click, focusable, etc.)
1290    /// via struct‑update syntax: `HitRegion { focusable: true, ..from_modifier(..) }`.
1291    pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
1292        Self {
1293            id,
1294            rect,
1295            z_index: m.z_index,
1296            on_click: m.on_click.clone(),
1297            on_double_click: m.on_double_click.clone(),
1298            on_long_click: m.on_long_click.clone(),
1299            on_pointer_down: m.on_pointer_down.clone(),
1300            on_pointer_move: m.on_pointer_move.clone(),
1301            on_pointer_up: m.on_pointer_up.clone(),
1302            on_pointer_cancel: m.on_pointer_cancel.clone(),
1303            on_pointer_enter: m.on_pointer_enter.clone(),
1304            on_pointer_leave: m.on_pointer_leave.clone(),
1305            on_action: m.on_action.clone(),
1306            on_set_progress: m.on_set_progress.clone(),
1307            on_expand: m.on_expand.clone(),
1308            on_collapse: m.on_collapse.clone(),
1309            on_dismiss: m.on_dismiss.clone(),
1310            on_key_event: m.on_key_event.clone(),
1311            on_preview_key_event: m.on_preview_key_event.clone(),
1312            on_ime: m.on_ime.clone(),
1313            cursor: m.cursor.clone(),
1314            on_drag_start: m.on_drag_start.clone(),
1315            on_drag_end: m.on_drag_end.clone(),
1316            on_drag_enter: m.on_drag_enter.clone(),
1317            on_drag_over: m.on_drag_over.clone(),
1318            on_drag_leave: m.on_drag_leave.clone(),
1319            on_scroll: m.on_scroll.clone(),
1320            on_scroll_with_source: m.on_scroll_with_source.clone(),
1321            on_drop: m.on_drop.clone(),
1322            drag_preview: m.drag_preview.clone(),
1323            disabled: m.disabled,
1324            tf_enabled: true,
1325            tf_read_only: false,
1326            ..Default::default()
1327        }
1328    }
1329}
1330
1331/// Route a gesture action to the topmost hit region under `center`,
1332/// walking up ancestors until one consumes it. Fallback for gestures that
1333/// miss the focused widget, matching Compose routing to the node under the
1334/// pointer. Coordinates are physical px, like [`HitRegion::rect`].
1335pub fn route_action_to_point(
1336    hits: &[HitRegion],
1337    action: &crate::shortcuts::Action,
1338    center: crate::Vec2,
1339) -> bool {
1340    let mut best: Option<&HitRegion> = None;
1341    for h in hits {
1342        if h.disabled || h.on_action.is_none() {
1343            continue;
1344        }
1345        let r = h.rect;
1346        if center.x < r.x || center.x > r.x + r.w || center.y < r.y || center.y > r.y + r.h {
1347            continue;
1348        }
1349        let better = match best {
1350            None => true,
1351            Some(b) => (h.z_index, h.depth) >= (b.z_index, b.depth),
1352        };
1353        if better {
1354            best = Some(h);
1355        }
1356    }
1357    let mut current = best.map(|h| h.id);
1358    while let Some(id) = current {
1359        let Some(h) = hits.iter().find(|h| h.id == id) else {
1360            break;
1361        };
1362        if !h.disabled
1363            && let Some(cb) = &h.on_action
1364            && cb(action.clone())
1365        {
1366            return true;
1367        }
1368        current = h.parent;
1369    }
1370    false
1371}
1372
1373/// Flattened semantics node produced by `layout_and_paint`.
1374///
1375/// This is the source of truth for accessibility backends: it contains the
1376/// resolved screen rect, role, label, and focus/enabled state.
1377///
1378/// The platform runner should convert this into OS‑specific accessibility trees (when implemented)
1379/// (AT‑SPI on Linux, TalkBack on Android, etc.).
1380#[derive(Clone)]
1381pub struct SemNode {
1382    /// Stable id, shared with the associated `HitRegion` / `ViewId`.
1383    pub id: u64,
1384
1385    /// `None` means direct child of the window root.
1386    pub parent: Option<u64>,
1387
1388    pub role: Role,
1389    pub label: Option<String>,
1390    pub rect: Rect,
1391    pub focused: bool,
1392    pub enabled: bool,
1393    /// Marks this node as a collection of selectable children (e.g., Tabs).
1394    pub selectable_group: bool,
1395    pub checked: Option<bool>,
1396    pub selected: Option<bool>,
1397    pub value: Option<String>,
1398    pub heading: bool,
1399    pub live_region: Option<crate::semantics::LiveRegion>,
1400    pub state_description: Option<String>,
1401    pub traversal_index: Option<f32>,
1402    pub is_traversal_group: bool,
1403    pub hidden: bool,
1404    pub collection_rows: Option<usize>,
1405    pub collection_columns: Option<usize>,
1406    pub row_index: Option<usize>,
1407    pub column_index: Option<usize>,
1408    pub progress: Option<crate::semantics::ProgressRangeInfo>,
1409    pub error: Option<String>,
1410    pub expanded: Option<bool>,
1411    pub scrollable: bool,
1412    pub can_expand: bool,
1413    pub can_collapse: bool,
1414    pub can_dismiss: bool,
1415}
1416
1417impl Default for SemNode {
1418    fn default() -> Self {
1419        Self {
1420            id: 0,
1421            parent: None,
1422            role: Role::default(),
1423            label: None,
1424            rect: Rect::default(),
1425            focused: false,
1426            enabled: true,
1427            selectable_group: false,
1428            checked: None,
1429            selected: None,
1430            value: None,
1431            heading: false,
1432            live_region: None,
1433            state_description: None,
1434            traversal_index: None,
1435            is_traversal_group: false,
1436            hidden: false,
1437            collection_rows: None,
1438            collection_columns: None,
1439            row_index: None,
1440            column_index: None,
1441            progress: None,
1442            error: None,
1443            expanded: None,
1444            scrollable: false,
1445            can_expand: false,
1446            can_collapse: false,
1447            can_dismiss: false,
1448        }
1449    }
1450}
1451
1452pub struct Scheduler {
1453    next_id: u64,
1454    /// Per-scope unique IDs, assigned lazily when a scope first executes.
1455    /// Keyed by the scope key string from `scope!`.
1456    scope_key_to_id: FxHashMap<String, u32>,
1457    next_scope_id: u32,
1458    /// Stack of active scope keys. `id()` allocates from the innermost scope
1459    /// so nested `scope!` bodies get stable packed IDs and the outer scope
1460    /// resumes correctly after the inner exits (previously `exit_scope` reset
1461    /// to `None`, leaking outer IDs into the global sequence).
1462    current_scope: Vec<String>,
1463    /// Per-scope local ID counters. Reset to 0 when a scope re-executes.
1464    scope_local_counters: FxHashMap<String, u32>,
1465    pub focused: Option<u64>,
1466    pub size: (u32, u32),
1467    /// Last known mouse pointer position in physical px, updated on
1468    /// mouse move/press/release without passing through focus dispatch.
1469    pub pointer_pos_px: Option<(f32, f32)>,
1470    /// Polled physical keys currently down, layout-independent
1471    /// positions (see [`PhysicalKey`]). The platform runner maintains
1472    /// this from raw `KeyboardInput` events without passing through
1473    /// focus dispatch; games reconcile their event-staged held sets
1474    /// against it every frame (GML `keyboard_check` parity). Cleared
1475    /// on window focus loss (no key-ups arrive across an alt-tab).
1476    pub held_keys: HashSet<PhysicalKey>,
1477    /// Window focus as of the last platform event. `false` drops every
1478    /// held key on the game side.
1479    pub window_focused: bool,
1480    /// Live touch contacts in physical px: `(touch id, x, y)` per
1481    /// finger currently down. Written by the platform runners from
1482    /// raw `WindowEvent::Touch` (Started/Moved/Ended-Cancelled), read
1483    /// by games through `feed_polled`-style staging snapshots
1484    /// (GML `device_mouse_x_to_gui(i)` parity: stable per-finger slot
1485    /// ids, positions sampled every tick while held).
1486    pub touch_points: Vec<(u64, f32, f32)>,
1487    /// Polled mouse-button levels, same source as `held_keys`
1488    /// (GML `mouse_check_button` parity; repairs a missed button-up
1489    /// when the release lands outside the window).
1490    pub mouse_primary: bool,
1491    pub mouse_secondary: bool,
1492    pub mouse_middle: bool,
1493    /// App-requested cursor override. When `Some`, the platform
1494    /// applies it instead of the hover-derived icon: games hide the
1495    /// OS pointer here (`CursorIcon::Hidden`) while drawing their own
1496    /// crosshair, GML `window_set_cursor(cr_none)` parity. `None`
1497    /// restores hover behavior. Set during composition (any view can
1498    /// write it; last write per frame wins), consumed by the runner
1499    /// after `frame()`.
1500    pub cursor_override: Option<CursorIcon>,
1501}
1502
1503impl Default for Scheduler {
1504    fn default() -> Self {
1505        Self::new()
1506    }
1507}
1508
1509impl Scheduler {
1510    pub fn new() -> Self {
1511        Self {
1512            next_id: 1,
1513            scope_key_to_id: FxHashMap::default(),
1514            next_scope_id: 1,
1515            current_scope: Vec::new(),
1516            scope_local_counters: FxHashMap::default(),
1517            focused: None,
1518            size: (1280, 800),
1519            pointer_pos_px: None,
1520            held_keys: HashSet::new(),
1521            window_focused: true,
1522            touch_points: Vec::new(),
1523            mouse_primary: false,
1524            mouse_secondary: false,
1525            mouse_middle: false,
1526            cursor_override: None,
1527        }
1528    }
1529
1530    /// Enter a named scope. Subsequent `id()` calls within this scope
1531    /// will allocate from the scope's local counter, producing packed
1532    /// `(scope_id << 32) | local_id` values that are stable across sibling
1533    /// recompositions. Nested scopes push; `exit_scope` pops.
1534    pub fn enter_scope(&mut self, key: &str) {
1535        if !self.current_scope.iter().any(|k| k == key) {
1536            self.scope_local_counters.insert(key.to_string(), 0);
1537        }
1538        self.current_scope.push(key.to_string());
1539        self.get_or_create_scope_id(key);
1540    }
1541
1542    /// Exit the innermost scope. No-op if the stack is empty or the top does
1543    /// not match (defensive: never corrupt an outer scope).
1544    pub fn exit_scope(&mut self) {
1545        self.current_scope.pop();
1546    }
1547
1548    /// RAII scope entry: pops on drop, so a panicking scope body cannot leave
1549    /// the scheduler stuck in the wrong scope (which previously leaked outer
1550    /// IDs into the global sequence).
1551    pub fn scope_guard<'a>(&'a mut self, key: &str) -> SchedulerScopeGuard<'a> {
1552        self.enter_scope(key);
1553        SchedulerScopeGuard { sched: self }
1554    }
1555
1556    /// Panic-safe scope entry that does NOT hold a borrow: the guarded body
1557    /// (including nested `scope!`) can keep using the `Scheduler`. Used by
1558    /// the `scope!` macro.
1559    pub fn scope_guard_raw(&mut self, key: &str) -> SchedulerScopeGuardRaw {
1560        let ptr = self as *mut Scheduler;
1561        unsafe { SchedulerScopeGuardRaw::enter(ptr, key) }
1562    }
1563
1564    fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
1565        if let Some(&id) = self.scope_key_to_id.get(key) {
1566            id
1567        } else {
1568            let id = self.next_scope_id;
1569            self.next_scope_id += 1;
1570            self.scope_key_to_id.insert(key.to_string(), id);
1571            id
1572        }
1573    }
1574
1575    pub fn id(&mut self) -> u64 {
1576        if let Some(key) = self.current_scope.last().cloned() {
1577            let scope_id = self.scope_key_to_id.get(&key).copied().unwrap_or(0);
1578            let local = self.scope_local_counters.get_mut(&key).unwrap();
1579            let id = *local;
1580            *local += 1;
1581            (scope_id as u64) << 32 | id as u64
1582        } else {
1583            // Global sequential ID (for non-scoped views)
1584            let id = self.next_id;
1585            self.next_id += 1;
1586            id
1587        }
1588    }
1589
1590    pub fn id_count(&self) -> u64 {
1591        self.next_id - 1
1592    }
1593
1594    /// True while the named physical key is in the polled snapshot.
1595    pub fn is_held(&self, key: PhysicalKey) -> bool {
1596        self.held_keys.contains(&key)
1597    }
1598
1599    /// Snapshot the current ID counter (before executing a scope body) so the
1600    /// delta can be computed after the body returns.
1601    pub fn snapshot_id(&self) -> u64 {
1602        self.next_id
1603    }
1604
1605    /// Advance the ID counter by `count` without assigning IDs.
1606    /// Used by the scope! macro to reserve IDs for a cached scope subtree.
1607    pub fn advance_id(&mut self, count: u32) {
1608        self.next_id += count as u64;
1609    }
1610
1611    /// Number of IDs assigned since `prev_id` (the value returned by
1612    /// `snapshot_id()` before executing a scope body).
1613    pub fn ids_used_since(&self, prev_id: u64) -> u32 {
1614        (self.next_id - prev_id) as u32
1615    }
1616
1617    pub fn gc_scope_state(&mut self, live_keys: &rustc_hash::FxHashSet<String>) {
1618        self.scope_key_to_id
1619            .retain(|key, _| live_keys.contains(key));
1620        self.scope_local_counters
1621            .retain(|key, _| live_keys.contains(key));
1622    }
1623}
1624
1625/// RAII guard from [`Scheduler::scope_guard`]. Pops the scope on drop.
1626pub struct SchedulerScopeGuard<'a> {
1627    sched: &'a mut Scheduler,
1628}
1629
1630impl Drop for SchedulerScopeGuard<'_> {
1631    fn drop(&mut self) {
1632        self.sched.exit_scope();
1633    }
1634}
1635
1636/// Panic-safe scope guard that does NOT hold a borrow across the body.
1637///
1638/// The `scope!` macro uses this (not [`SchedulerScopeGuard`]) so the guarded
1639/// body, including nested `scope!` invocations, can keep using the
1640/// `Scheduler` normally. At drop time (normal or unwind) no other borrows of
1641/// the scheduler are live, since the body has ended.
1642pub struct SchedulerScopeGuardRaw {
1643    sched: *mut Scheduler,
1644}
1645
1646impl SchedulerScopeGuardRaw {
1647    /// # Safety
1648    /// `sched` must point to a valid `Scheduler` for the guard's lifetime,
1649    /// and the scheduler must not be used while the guard is being dropped
1650    /// (guaranteed when the guard outlives the body it protects).
1651    pub unsafe fn enter(sched: *mut Scheduler, key: &str) -> Self {
1652        unsafe {
1653            (*sched).enter_scope(key);
1654        }
1655        Self { sched }
1656    }
1657}
1658
1659impl Drop for SchedulerScopeGuardRaw {
1660    fn drop(&mut self) {
1661        unsafe {
1662            (*self.sched).exit_scope();
1663        }
1664    }
1665}
1666
1667impl Scheduler {
1668    pub fn repose<F>(
1669        &mut self,
1670        mut build_root: F,
1671        layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
1672    ) -> Frame
1673    where
1674        F: FnMut(&mut Scheduler) -> View,
1675    {
1676        let guard = ComposeGuard::begin();
1677        let root = guard.scope.run(|| build_root(self));
1678        let (scene, hits, sem) = layout_paint(&root, self.size);
1679
1680        let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
1681
1682        let live_scope_keys = guard.live_scope_keys();
1683        let frame = Frame {
1684            scene,
1685            hit_regions: hits,
1686            semantics_nodes: sem,
1687            focus_chain,
1688        };
1689        drop(guard);
1690        self.gc_scope_state(&live_scope_keys);
1691        frame
1692    }
1693}
1694
1695/// Test helper: full composer reset. Production shutdown should call
1696/// `shutdown_composition`.
1697#[cfg(test)]
1698pub fn clear_composer() {
1699    shutdown_composition();
1700}
1701
1702#[cfg(test)]
1703mod focus_trap_tests {
1704    use super::*;
1705
1706    fn region(id: u64, x: f32, group: Option<u64>) -> HitRegion {
1707        HitRegion {
1708            id,
1709            rect: Rect {
1710                x,
1711                y: 0.0,
1712                w: 10.0,
1713                h: 10.0,
1714            },
1715            focus_group_id: group,
1716            ..Default::default()
1717        }
1718    }
1719
1720    #[test]
1721    fn arrows_stay_inside_group() {
1722        // Dialog buttons 2,3 in group 9; background button 4 outside;
1723        // outsider 1 sits left of button 2 and would win unconstrained.
1724        let chain = vec![1, 2, 3, 4];
1725        let regions = vec![
1726            region(1, 0.0, None),
1727            region(2, 20.0, Some(9)),
1728            region(3, 40.0, Some(9)),
1729            region(4, 60.0, None),
1730        ];
1731        assert_eq!(
1732            spatial_focus_next(&chain, &regions, Some(2), FocusDirection::Left),
1733            None,
1734            "outsider 1 is left of 2 but outside the group: trapped"
1735        );
1736        assert_eq!(
1737            spatial_focus_next(&chain, &regions, Some(2), FocusDirection::Right),
1738            Some(3)
1739        );
1740        assert_eq!(
1741            spatial_focus_next(&chain, &regions, Some(3), FocusDirection::Left),
1742            Some(2)
1743        );
1744        assert_eq!(
1745            spatial_focus_next(&chain, &regions, Some(1), FocusDirection::Right),
1746            Some(2),
1747            "ungrouped focus still sees the full chain"
1748        );
1749    }
1750
1751    #[test]
1752    fn tab_cycles_inside_group() {
1753        let chain = vec![1, 2, 3, 4];
1754        let regions = vec![
1755            region(1, 0.0, None),
1756            region(2, 20.0, Some(9)),
1757            region(3, 40.0, Some(9)),
1758            region(4, 60.0, None),
1759        ];
1760        let mut fm = FocusManager::new(chain, Some(2));
1761        fm.hit_regions = regions;
1762        assert_eq!(fm.move_tab(false), Some(3));
1763        assert_eq!(fm.move_tab(false), Some(2));
1764        assert_eq!(fm.move_tab(true), Some(3));
1765    }
1766
1767    #[test]
1768    fn tab_from_outside_can_enter_group() {
1769        let chain = vec![1, 2, 3, 4];
1770        let regions = vec![
1771            region(1, 0.0, None),
1772            region(2, 20.0, Some(9)),
1773            region(3, 40.0, Some(9)),
1774            region(4, 60.0, None),
1775        ];
1776        let mut fm = FocusManager::new(chain, Some(1));
1777        fm.hit_regions = regions;
1778        assert_eq!(fm.move_tab(false), Some(2));
1779        let mut fm = FocusManager::new(vec![1, 2, 3, 4], Some(1));
1780        fm.hit_regions = vec![
1781            region(1, 0.0, None),
1782            region(2, 20.0, Some(9)),
1783            region(3, 40.0, Some(9)),
1784            region(4, 60.0, None),
1785        ];
1786        assert_eq!(fm.move_tab(true), Some(4));
1787    }
1788
1789    #[test]
1790    fn empty_group_never_moves() {
1791        let chain = vec![1, 4];
1792        let regions = vec![region(1, 0.0, None), region(4, 60.0, None)];
1793        let mut fm = FocusManager::new(chain, Some(1));
1794        fm.hit_regions = regions.clone();
1795        assert_eq!(fm.move_tab(false), Some(4));
1796        let chain = vec![1, 2, 4];
1797        let regions = vec![
1798            region(1, 0.0, None),
1799            region(2, 20.0, Some(77)),
1800            region(4, 60.0, None),
1801        ];
1802        let mut fm = FocusManager::new(chain, Some(2));
1803        fm.hit_regions = regions;
1804        assert_eq!(fm.move_tab(false), Some(2));
1805        assert_eq!(
1806            spatial_focus_next(&fm.chain, &fm.hit_regions, Some(2), FocusDirection::Right),
1807            None
1808        );
1809    }
1810}