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