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            if let Some(entry) = registry.remove(key) {
166                to_run = Some(entry.disposer);
167                registry_removed = true;
168            }
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    let disposers = KEYED_DISPOSERS
225        .try_with(|registry| {
226            registry.try_borrow_mut().ok().map(|mut registry| {
227                let mut result = Vec::with_capacity(registry.len());
228                for key in order.into_iter().rev() {
229                    if let Some(entry) = registry.remove(&key) {
230                        result.push(entry.disposer);
231                    }
232                }
233                result.extend(registry.drain().map(|(_, entry)| entry.disposer));
234                result
235            })
236        })
237        .ok()
238        .flatten()
239        .unwrap_or_default();
240    disposers
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    pub live_keyed_owners: rustc_hash::FxHashSet<String>,
564    pub scope_caches: FxHashMap<String, crate::scope_cache::ScopeCache>,
565    pub live_scope_keys: rustc_hash::FxHashSet<String>,
566}
567
568pub struct ComposeGuard {
569    scope: Scope,
570}
571
572pub(crate) fn keyed_disposer_order(key: &str) -> usize {
573    KEYED_DISPOSER_ORDER
574        .try_with(|order| {
575            order
576                .try_borrow()
577                .ok()
578                .and_then(|order| order.iter().position(|registered| registered == key))
579        })
580        .ok()
581        .flatten()
582        .unwrap_or(usize::MAX)
583}
584
585pub(crate) fn release_keyed_owner(key: &str, owner: &str) -> (bool, Option<Box<dyn Any>>) {
586    let result = COMPOSER
587        .try_with(|composer| {
588            let mut composer = match composer.try_borrow_mut() {
589                Ok(composer) => composer,
590                Err(_) => return (false, None, None),
591            };
592            let owners = match composer.keyed_owners.get_mut(key) {
593                Some(owners) => owners,
594                None => return (false, None, None),
595            };
596            owners.remove(owner);
597            if !owners.is_empty() {
598                return (true, None, None);
599            }
600            composer.keyed_owners.remove(key);
601            let old_owner = composer.keyed_owner.remove(key);
602            (true, composer.keyed_slots.remove(key), old_owner)
603        })
604        .unwrap_or((false, None, None));
605    drop(result.2);
606    (result.0, result.1)
607}
608
609pub(crate) fn take_dead_keyed_slots(composer: &mut Composer) -> Vec<(String, Box<dyn Any>)> {
610    let dead: Vec<String> = composer
611        .keyed_slots
612        .keys()
613        .filter(|key| {
614            let live = composer
615                .keyed_owners
616                .get(*key)
617                .map(|owners| {
618                    owners
619                        .iter()
620                        .any(|owner| composer.live_keyed_owners.contains(owner))
621                })
622                .or_else(|| {
623                    composer
624                        .keyed_owner
625                        .get(*key)
626                        .map(|owner| composer.live_keyed_owners.contains(owner))
627                })
628                .unwrap_or(false);
629            !live
630        })
631        .cloned()
632        .collect();
633    let mut removed = Vec::with_capacity(dead.len());
634    for key in dead {
635        if let Some(value) = composer.keyed_slots.remove(&key) {
636            removed.push((key.clone(), value));
637        }
638        composer.keyed_owner.remove(&key);
639        composer.keyed_owners.remove(&key);
640    }
641    removed
642}
643
644pub(crate) fn current_scope_key_for_remember() -> Option<String> {
645    crate::scope_cache::current_scope_key()
646}
647
648fn flush_initializer_cursor() {
649    let pending = PENDING_INITIALIZER_CURSOR
650        .try_with(|pending| {
651            pending
652                .try_borrow_mut()
653                .ok()
654                .and_then(|mut pending| pending.take())
655        })
656        .ok()
657        .flatten();
658    let Some(cursor) = pending else {
659        return;
660    };
661    let restored = COMPOSER
662        .try_with(|composer| {
663            composer
664                .try_borrow_mut()
665                .ok()
666                .map(|mut composer| composer.cursor = cursor)
667        })
668        .ok()
669        .flatten();
670    if restored.is_none() {
671        let _ = PENDING_INITIALIZER_CURSOR.try_with(|pending| {
672            if let Ok(mut pending) = pending.try_borrow_mut() {
673                *pending = Some(cursor);
674            }
675        });
676    }
677}
678
679impl ComposeGuard {
680    pub fn begin() -> Self {
681        flush_initializer_cursor();
682        COMPOSER.with(|c| {
683            let mut c = c.borrow_mut();
684            c.cursor = 0;
685            c.auto_counts.clear();
686            c.live_scope_keys.clear();
687            c.live_keyed_owners.clear();
688            c.live_scope_keys.insert(String::new());
689            c.live_keyed_owners.insert(scope_owner_token(None));
690        });
691
692        let scope = ROOT_SCOPE.with(|rs| {
693            if let Some(existing) = rs.borrow().clone() {
694                existing
695            } else {
696                let s = Scope::new();
697                *rs.borrow_mut() = Some(s.clone());
698                s
699            }
700        });
701
702        ComposeGuard { scope }
703    }
704
705    pub fn scope(&self) -> &Scope {
706        &self.scope
707    }
708
709    pub fn live_scope_keys(&self) -> rustc_hash::FxHashSet<String> {
710        COMPOSER.with(|c| c.borrow().live_scope_keys.clone())
711    }
712}
713
714impl Drop for ComposeGuard {
715    fn drop(&mut self) {
716        let removed = COMPOSER.with(|c| {
717            let mut c = c.borrow_mut();
718            let n = c.cursor;
719            let mut removed = Vec::new();
720            if c.slots.len() > n {
721                removed.extend(c.slots.drain(n..));
722            }
723            if c.slot_callers.len() > n {
724                c.slot_callers.truncate(n);
725            }
726            removed
727        });
728        drop(removed);
729        crate::scope_cache::gc_dead_scopes();
730    }
731}
732
733/// Dispose the root composition scope and clear all composer caches.
734///
735/// Call once on process exit (desktop `exiting`, tests). After this the next
736/// `ComposeGuard::begin` starts from a fresh root scope.
737pub fn shutdown_composition() {
738    let scope = ROOT_SCOPE.with(|rs| rs.borrow_mut().take());
739    if let Some(scope) = scope {
740        scope.dispose();
741    }
742    let removed = COMPOSER.with(|c| {
743        let mut c = c.borrow_mut();
744        let slots = std::mem::take(&mut c.slots);
745        let callers = std::mem::take(&mut c.slot_callers);
746        let keyed = std::mem::take(&mut c.keyed_slots);
747        let caches = std::mem::take(&mut c.scope_caches);
748        c.keyed_owner.clear();
749        c.keyed_owners.clear();
750        c.live_keyed_owners.clear();
751        c.live_scope_keys.clear();
752        c.cursor = 0;
753        c.auto_counts.clear();
754        (slots, callers, keyed, caches)
755    });
756    drop(removed);
757    for disposer in take_all_keyed_disposers() {
758        run_keyed_disposer(disposer);
759    }
760    crate::animation_driver::shutdown();
761    crate::scope_cache::clear_all_scope_deps();
762    INITIALIZER_STACK.with(|stack| stack.borrow_mut().clear());
763    PENDING_INITIALIZER_CURSOR.with(|pending| {
764        let _ = pending.borrow_mut().take();
765    });
766}
767
768struct InitializerGuard {
769    active: bool,
770    cursor: Option<usize>,
771}
772
773impl InitializerGuard {
774    fn assert_sequential_allowed() {
775        assert!(
776            INITIALIZER_STACK.with(|stack| stack
777                .try_borrow()
778                .map(|stack| stack.is_empty())
779                .unwrap_or(false)),
780            "remember initializer re-entered composition; use an explicit keyed helper"
781        );
782    }
783
784    fn enter_sequential() -> (Self, usize) {
785        Self::assert_sequential_allowed();
786        let cursor = COMPOSER.with(|composer| {
787            let mut composer = composer.borrow_mut();
788            let cursor = composer.cursor;
789            composer.cursor = composer.cursor.wrapping_add(1);
790            cursor
791        });
792        let pushed = INITIALIZER_STACK
793            .try_with(|stack| {
794                stack.try_borrow_mut().ok().map(|mut stack| {
795                    stack.push(InitializerKind::Sequential);
796                    true
797                })
798            })
799            .ok()
800            .flatten();
801        if pushed != Some(true) {
802            let _ = COMPOSER.try_with(|composer| {
803                if let Ok(mut composer) = composer.try_borrow_mut() {
804                    composer.cursor = cursor;
805                }
806            });
807            panic!("initializer stack busy");
808        }
809        (
810            Self {
811                active: true,
812                cursor: Some(cursor),
813            },
814            cursor,
815        )
816    }
817
818    fn assert_keyed_allowed() {
819        assert!(
820            INITIALIZER_STACK.with(|stack| stack
821                .try_borrow()
822                .map(|stack| stack.is_empty())
823                .unwrap_or(false)),
824            "remember initializer re-entered composition; use an explicit keyed helper"
825        );
826    }
827
828    fn enter_keyed() -> Self {
829        Self::assert_keyed_allowed();
830        INITIALIZER_STACK.with(|stack| {
831            stack
832                .try_borrow_mut()
833                .expect("initializer stack busy")
834                .push(InitializerKind::Keyed)
835        });
836        Self {
837            active: true,
838            cursor: None,
839        }
840    }
841
842    fn commit(&mut self) {
843        if !self.active {
844            return;
845        }
846        let _ = INITIALIZER_STACK.try_with(|stack| {
847            if let Ok(mut stack) = stack.try_borrow_mut() {
848                stack.pop();
849            }
850        });
851        self.active = false;
852    }
853}
854
855impl Drop for InitializerGuard {
856    fn drop(&mut self) {
857        if !self.active {
858            return;
859        }
860        if let Some(cursor) = self.cursor {
861            let restored = COMPOSER
862                .try_with(|composer| {
863                    composer
864                        .try_borrow_mut()
865                        .ok()
866                        .map(|mut composer| composer.cursor = cursor)
867                })
868                .ok()
869                .flatten();
870            if restored.is_none() {
871                let _ = PENDING_INITIALIZER_CURSOR.try_with(|pending| {
872                    if let Ok(mut pending) = pending.try_borrow_mut() {
873                        *pending = Some(cursor);
874                    }
875                });
876            }
877        }
878        let _ = INITIALIZER_STACK.try_with(|stack| {
879            if let Ok(mut stack) = stack.try_borrow_mut() {
880                stack.pop();
881            }
882        });
883    }
884}
885
886/// Slot-based remember (sequential composition only).
887/// This prevents state aliasing when the composition tree structure changes between frames
888#[track_caller]
889pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
890    flush_initializer_cursor();
891    let caller = Location::caller();
892    let (mut initializer, cursor) = InitializerGuard::enter_sequential();
893
894    let existing = COMPOSER.with(|c| {
895        let c = c.borrow();
896        if cursor >= c.slots.len() || c.slot_callers.get(cursor).copied() != Some(caller) {
897            return None;
898        }
899        c.slots[cursor].downcast_ref::<Rc<T>>().cloned()
900    });
901    if let Some(rc) = existing {
902        initializer.commit();
903        return rc;
904    }
905
906    if COMPOSER.with(|c| {
907        let c = c.borrow();
908        cursor < c.slots.len() && c.slot_callers.get(cursor).copied() == Some(caller)
909    }) {
910        log::warn!(
911            "remember: slot {} type changed {}. \
912             Use remember_with_key(key, || ...) for conditional branches.",
913            cursor,
914            std::any::type_name::<T>(),
915        );
916    }
917
918    let rc = Rc::new(init());
919    let old = COMPOSER.with(|c| {
920        let mut c = c.borrow_mut();
921        let value: Box<dyn Any> = Box::new(rc.clone());
922        let old = if cursor < c.slots.len() {
923            Some(std::mem::replace(&mut c.slots[cursor], value))
924        } else {
925            c.slots.push(value);
926            None
927        };
928        if cursor < c.slot_callers.len() {
929            c.slot_callers[cursor] = caller;
930        } else {
931            c.slot_callers.push(caller);
932        }
933        old
934    });
935    initializer.commit();
936    drop(old);
937    rc
938}
939
940fn push_key_part(key: &mut String, part: &str) {
941    key.push_str(&part.len().to_string());
942    key.push(':');
943    key.push_str(part);
944    key.push('|');
945}
946
947#[track_caller]
948pub fn remember_auto<T: 'static>(slot: &str, init: impl FnOnce() -> T) -> Rc<T> {
949    let caller = Location::caller();
950    let scope = current_scope_key_for_remember().unwrap_or_default();
951    let mut base = String::from("auto:v1|");
952    push_key_part(&mut base, &scope);
953    push_key_part(&mut base, caller.file());
954    push_key_part(&mut base, &caller.line().to_string());
955    push_key_part(&mut base, &caller.column().to_string());
956    push_key_part(&mut base, slot);
957    let occurrence = COMPOSER.with(|composer| {
958        let mut composer = composer.borrow_mut();
959        let count = composer.auto_counts.entry(base.clone()).or_insert(0);
960        let occurrence = *count;
961        *count += 1;
962        occurrence
963    });
964    push_key_part(&mut base, &occurrence.to_string());
965    remember_with_key(base, init)
966}
967
968pub fn keyed<T>(key: impl Into<String>, f: impl FnOnce() -> T) -> T {
969    let key = key.into();
970    assert!(!key.is_empty(), "keyed scopes need non-empty keys");
971    let parent = current_scope_key_for_remember().unwrap_or_default();
972    let mut full_key = String::new();
973    push_key_part(&mut full_key, &parent);
974    push_key_part(&mut full_key, &key);
975    crate::scope_cache::with_scope_key(&full_key, f)
976}
977
978/// Key-based remember.
979pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
980    flush_initializer_cursor();
981    InitializerGuard::assert_keyed_allowed();
982    let key = key.into();
983    let scope = current_scope_key_for_remember();
984    let owner = scope_owner_token(scope.as_deref());
985    let existing = COMPOSER.with(|composer| {
986        let composer = composer.borrow();
987        composer
988            .keyed_slots
989            .get(&key)
990            .and_then(|value| value.downcast_ref::<Rc<T>>().cloned())
991    });
992    if let Some(existing) = existing {
993        let old_owner = COMPOSER.with(|composer| {
994            let mut composer = composer.borrow_mut();
995            let old_owner = composer.keyed_owner.insert(key.clone(), owner.clone());
996            composer
997                .keyed_owners
998                .entry(key.clone())
999                .or_default()
1000                .insert(owner.clone());
1001            composer.live_keyed_owners.insert(owner);
1002            old_owner
1003        });
1004        drop(old_owner);
1005        return existing;
1006    }
1007
1008    let has_wrong_type = COMPOSER.with(|composer| {
1009        let composer = composer.borrow();
1010        if composer.keyed_slots.contains_key(&key) {
1011            log::warn!(
1012                "remember_with_key: key '{}' reused with a different type; replacing.",
1013                key
1014            );
1015        }
1016        cfg!(debug_assertions) && composer.keyed_slots.len() > 10_000
1017    });
1018    if has_wrong_type {
1019        log::warn!(
1020            "remember_with_key: more than 10k keys stored; are you generating unbounded dynamic keys?"
1021        );
1022    }
1023
1024    let mut initializer = InitializerGuard::enter_keyed();
1025    let value = Rc::new(init());
1026    let (result, old) = COMPOSER.with(|composer| {
1027        let mut composer = composer.borrow_mut();
1028        if let Some(existing) = composer.keyed_slots.get(&key)
1029            && let Some(existing) = existing.downcast_ref::<Rc<T>>()
1030        {
1031            let existing = existing.clone();
1032            composer.keyed_owner.insert(key.clone(), owner.clone());
1033            composer
1034                .keyed_owners
1035                .entry(key.clone())
1036                .or_default()
1037                .insert(owner.clone());
1038            composer.live_keyed_owners.insert(owner);
1039            return (existing, None);
1040        }
1041        let old = composer
1042            .keyed_slots
1043            .insert(key.clone(), Box::new(value.clone()));
1044        composer.keyed_owner.insert(key.clone(), owner.clone());
1045        composer
1046            .keyed_owners
1047            .entry(key)
1048            .or_default()
1049            .insert(owner.clone());
1050        (value, old)
1051    });
1052    initializer.commit();
1053    drop(old);
1054    result
1055}
1056
1057/// Raw slot state (`Rc<RefCell<T>>`). Writes via `borrow_mut()` don't request a
1058/// frame - prefer [`remember_mutable`] if a write must always recompose.
1059#[track_caller]
1060pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
1061    remember(|| RefCell::new(init()))
1062}
1063
1064/// Key-based variant of [`remember_state`]. Same no-frame-on-write caveat.
1065pub fn remember_state_with_key<T: 'static>(
1066    key: impl Into<String>,
1067    init: impl FnOnce() -> T,
1068) -> Rc<RefCell<T>> {
1069    remember_with_key(key, || RefCell::new(init()))
1070}
1071
1072/// Frame - output of composition for a tick: scene + input/semantics.
1073#[derive(Clone)]
1074pub struct Frame {
1075    pub scene: Scene,
1076    pub hit_regions: Vec<HitRegion>,
1077    pub semantics_nodes: Vec<SemNode>,
1078    pub focus_chain: Vec<u64>,
1079}
1080
1081/// Hit-test region in physical pixels (`rect` carries px magnitudes,
1082/// like Compose `Rect`).
1083#[derive(Clone, Default)]
1084pub struct HitRegion {
1085    pub id: u64,
1086    pub rect: Rect,
1087    /// Tree depth: 0 = root, higher = deeper child. Used for three-pass
1088    /// pointer dispatch to determine ancestor/descendant ordering.
1089    pub depth: u32,
1090    pub parent: Option<u64>,
1091    pub on_click: Option<Rc<dyn Fn()>>,
1092    pub on_double_click: Option<Rc<dyn Fn()>>,
1093    pub on_long_click: Option<Rc<dyn Fn()>>,
1094    pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
1095    pub focusable: bool,
1096    pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1097    pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1098    pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1099    pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1100    pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1101    pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1102    pub z_index: f32,
1103    pub disabled: bool,
1104    pub on_text_change: Option<Rc<dyn Fn(String)>>,
1105    pub on_text_submit: Option<Rc<dyn Fn(String)>>,
1106    /// If this hit region belongs to a TextField, this persistent key is used
1107    /// for looking up platform-managed TextFieldState. Falls back to `id` if None.
1108    pub tf_state_key: Option<u64>,
1109
1110    /// True if this hit region corresponds to a multiline text input (TextArea).
1111    pub tf_multiline: bool,
1112
1113    /// Unclipped top-left of the TextField *content* box (padding-inset).
1114    /// Used for pointer->grapheme mapping so parent scroll clipping of `rect`
1115    /// does not shift selection into the top of the content.
1116    /// `None` for non-textfields.
1117    pub tf_content_origin: Option<(f32, f32)>,
1118
1119    /// When false, the field rejects edits and is not focusable
1120    pub tf_enabled: bool,
1121
1122    /// When true, selection/focus/copy are allowed but mutations are rejected
1123    pub tf_read_only: bool,
1124
1125    pub tf_sensitive: bool,
1126
1127    /// Controlled text snapshot for this field (last compose).
1128    pub tf_value: String,
1129
1130    /// Font size for this text field in [`Sp`](crate::units::Sp)
1131    /// (for hit-test / caret mapping). `Sp::ZERO` means use `TF_FONT_SP` default.
1132    pub tf_font_size: crate::units::Sp,
1133
1134    // internal
1135    pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
1136    pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
1137    pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1138    pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1139    pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1140    pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
1141    /// Copied onto the drag session when a drag starts from this region.
1142    pub drag_preview: Option<crate::dnd::DragPreview>,
1143
1144    pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
1145
1146    /// Called when a key event is received while this element is focused.
1147    /// Return `true` to consume the event.
1148    pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
1149    /// Called before `on_key_event`. Return `true` to consume before normal dispatch.
1150    pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
1151
1152    /// Cursor hint for desktop/web.
1153    pub cursor: Option<crate::CursorIcon>,
1154
1155    /// If `Some(group_id)`, this hit region belongs to a focus group with the
1156    /// given id. Tab navigation will cycle within the group instead of moving
1157    /// to elements outside it. Set automatically by the layout engine when the
1158    /// element is a descendant of a node with `focus_group: true`.
1159    pub focus_group_id: Option<u64>,
1160
1161    /// IME keyboard hints, populated for text-field hit regions so the
1162    /// platform runner can configure the OS keyboard / IME on focus.
1163    pub keyboard_type: crate::text::KeyboardType,
1164    pub capitalization: crate::text::KeyboardCapitalization,
1165    pub ime_action: crate::text::ImeAction,
1166    /// Whether auto-correct is enabled. `None` = follow platform default;
1167    /// password keyboards always resolve to `false` in the layout engine.
1168    pub auto_correct: Option<bool>,
1169
1170    /// Shared interaction source auto-wired by the layout engine
1171    /// (press/hover/focus/drag). Used by keyboard activation and focus
1172    /// transitions so they stay in parity with pointer input.
1173    /// `None` when the component does not need one (no indication/state colors).
1174    pub interaction_source: Option<crate::modifier::InteractionSource>,
1175}
1176
1177impl HitRegion {
1178    /// Seed a HitRegion with all the modifier's event handlers + dnd + cursor.
1179    /// Call‑sites should only override the fields that differ (on_click, focusable, etc.)
1180    /// via struct‑update syntax: `HitRegion { focusable: true, ..from_modifier(..) }`.
1181    pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
1182        Self {
1183            id,
1184            rect,
1185            z_index: m.z_index,
1186            on_click: m.on_click.clone(),
1187            on_double_click: m.on_double_click.clone(),
1188            on_long_click: m.on_long_click.clone(),
1189            on_pointer_down: m.on_pointer_down.clone(),
1190            on_pointer_move: m.on_pointer_move.clone(),
1191            on_pointer_up: m.on_pointer_up.clone(),
1192            on_pointer_cancel: m.on_pointer_cancel.clone(),
1193            on_pointer_enter: m.on_pointer_enter.clone(),
1194            on_pointer_leave: m.on_pointer_leave.clone(),
1195            on_action: m.on_action.clone(),
1196            on_key_event: m.on_key_event.clone(),
1197            on_preview_key_event: m.on_preview_key_event.clone(),
1198            cursor: m.cursor.clone(),
1199            on_drag_start: m.on_drag_start.clone(),
1200            on_drag_end: m.on_drag_end.clone(),
1201            on_drag_enter: m.on_drag_enter.clone(),
1202            on_drag_over: m.on_drag_over.clone(),
1203            on_drag_leave: m.on_drag_leave.clone(),
1204            on_scroll: m.on_scroll.clone(),
1205            on_drop: m.on_drop.clone(),
1206            drag_preview: m.drag_preview.clone(),
1207            disabled: m.disabled,
1208            tf_enabled: true,
1209            tf_read_only: false,
1210            ..Default::default()
1211        }
1212    }
1213}
1214
1215/// Flattened semantics node produced by `layout_and_paint`.
1216///
1217/// This is the source of truth for accessibility backends: it contains the
1218/// resolved screen rect, role, label, and focus/enabled state.
1219///
1220/// The platform runner should convert this into OS‑specific accessibility trees (when implemented)
1221/// (AT‑SPI on Linux, TalkBack on Android, etc.).
1222#[derive(Clone)]
1223pub struct SemNode {
1224    /// Stable id, shared with the associated `HitRegion` / `ViewId`.
1225    pub id: u64,
1226
1227    /// `None` means direct child of the window root.
1228    pub parent: Option<u64>,
1229
1230    pub role: Role,
1231    pub label: Option<String>,
1232    pub rect: Rect,
1233    pub focused: bool,
1234    pub enabled: bool,
1235    /// Marks this node as a collection of selectable children (e.g., Tabs).
1236    pub selectable_group: bool,
1237    pub checked: Option<bool>,
1238    pub selected: Option<bool>,
1239    pub value: Option<String>,
1240}
1241
1242impl Default for SemNode {
1243    fn default() -> Self {
1244        Self {
1245            id: 0,
1246            parent: None,
1247            role: Role::default(),
1248            label: None,
1249            rect: Rect::default(),
1250            focused: false,
1251            enabled: true,
1252            selectable_group: false,
1253            checked: None,
1254            selected: None,
1255            value: None,
1256        }
1257    }
1258}
1259
1260pub struct Scheduler {
1261    next_id: u64,
1262    /// Per-scope unique IDs, assigned lazily when a scope first executes.
1263    /// Keyed by the scope key string from `scope!`.
1264    scope_key_to_id: FxHashMap<String, u32>,
1265    next_scope_id: u32,
1266    /// Stack of active scope keys. `id()` allocates from the innermost scope
1267    /// so nested `scope!` bodies get stable packed IDs and the outer scope
1268    /// resumes correctly after the inner exits (previously `exit_scope` reset
1269    /// to `None`, leaking outer IDs into the global sequence).
1270    current_scope: Vec<String>,
1271    /// Per-scope local ID counters. Reset to 0 when a scope re-executes.
1272    scope_local_counters: FxHashMap<String, u32>,
1273    pub focused: Option<u64>,
1274    pub size: (u32, u32),
1275    /// Last known mouse pointer position in physical px, updated on
1276    /// mouse move/press/release without passing through focus dispatch.
1277    pub pointer_pos_px: Option<(f32, f32)>,
1278    /// Polled physical keys currently down, layout-independent
1279    /// positions (see [`PhysicalKey`]). The platform runner maintains
1280    /// this from raw `KeyboardInput` events without passing through
1281    /// focus dispatch; games reconcile their event-staged held sets
1282    /// against it every frame (GML `keyboard_check` parity). Cleared
1283    /// on window focus loss (no key-ups arrive across an alt-tab).
1284    pub held_keys: HashSet<PhysicalKey>,
1285    /// Window focus as of the last platform event. `false` drops every
1286    /// held key on the game side.
1287    pub window_focused: bool,
1288    /// Live touch contacts in physical px: `(touch id, x, y)` per
1289    /// finger currently down. Written by the platform runners from
1290    /// raw `WindowEvent::Touch` (Started/Moved/Ended-Cancelled), read
1291    /// by games through `feed_polled`-style staging snapshots
1292    /// (GML `device_mouse_x_to_gui(i)` parity: stable per-finger slot
1293    /// ids, positions sampled every tick while held).
1294    pub touch_points: Vec<(u64, f32, f32)>,
1295    /// Polled mouse-button levels, same source as `held_keys`
1296    /// (GML `mouse_check_button` parity; repairs a missed button-up
1297    /// when the release lands outside the window).
1298    pub mouse_primary: bool,
1299    pub mouse_secondary: bool,
1300    pub mouse_middle: bool,
1301    /// App-requested cursor override. When `Some`, the platform
1302    /// applies it instead of the hover-derived icon: games hide the
1303    /// OS pointer here (`CursorIcon::Hidden`) while drawing their own
1304    /// crosshair, GML `window_set_cursor(cr_none)` parity. `None`
1305    /// restores hover behavior. Set during composition (any view can
1306    /// write it; last write per frame wins), consumed by the runner
1307    /// after `frame()`.
1308    pub cursor_override: Option<CursorIcon>,
1309}
1310
1311impl Default for Scheduler {
1312    fn default() -> Self {
1313        Self::new()
1314    }
1315}
1316
1317impl Scheduler {
1318    pub fn new() -> Self {
1319        Self {
1320            next_id: 1,
1321            scope_key_to_id: FxHashMap::default(),
1322            next_scope_id: 1,
1323            current_scope: Vec::new(),
1324            scope_local_counters: FxHashMap::default(),
1325            focused: None,
1326            size: (1280, 800),
1327            pointer_pos_px: None,
1328            held_keys: HashSet::new(),
1329            window_focused: true,
1330            touch_points: Vec::new(),
1331            mouse_primary: false,
1332            mouse_secondary: false,
1333            mouse_middle: false,
1334            cursor_override: None,
1335        }
1336    }
1337
1338    /// Enter a named scope. Subsequent `id()` calls within this scope
1339    /// will allocate from the scope's local counter, producing packed
1340    /// `(scope_id << 32) | local_id` values that are stable across sibling
1341    /// recompositions. Nested scopes push; `exit_scope` pops.
1342    pub fn enter_scope(&mut self, key: &str) {
1343        if !self.current_scope.iter().any(|k| k == key) {
1344            self.scope_local_counters.insert(key.to_string(), 0);
1345        }
1346        self.current_scope.push(key.to_string());
1347        self.get_or_create_scope_id(key);
1348    }
1349
1350    /// Exit the innermost scope. No-op if the stack is empty or the top does
1351    /// not match (defensive: never corrupt an outer scope).
1352    pub fn exit_scope(&mut self) {
1353        self.current_scope.pop();
1354    }
1355
1356    /// RAII scope entry: pops on drop, so a panicking scope body cannot leave
1357    /// the scheduler stuck in the wrong scope (which previously leaked outer
1358    /// IDs into the global sequence).
1359    pub fn scope_guard<'a>(&'a mut self, key: &str) -> SchedulerScopeGuard<'a> {
1360        self.enter_scope(key);
1361        SchedulerScopeGuard { sched: self }
1362    }
1363
1364    /// Panic-safe scope entry that does NOT hold a borrow: the guarded body
1365    /// (including nested `scope!`) can keep using the `Scheduler`. Used by
1366    /// the `scope!` macro.
1367    pub fn scope_guard_raw(&mut self, key: &str) -> SchedulerScopeGuardRaw {
1368        let ptr = self as *mut Scheduler;
1369        unsafe { SchedulerScopeGuardRaw::enter(ptr, key) }
1370    }
1371
1372    fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
1373        if let Some(&id) = self.scope_key_to_id.get(key) {
1374            id
1375        } else {
1376            let id = self.next_scope_id;
1377            self.next_scope_id += 1;
1378            self.scope_key_to_id.insert(key.to_string(), id);
1379            id
1380        }
1381    }
1382
1383    pub fn id(&mut self) -> u64 {
1384        if let Some(key) = self.current_scope.last().cloned() {
1385            let scope_id = self.scope_key_to_id.get(&key).copied().unwrap_or(0);
1386            let local = self.scope_local_counters.get_mut(&key).unwrap();
1387            let id = *local;
1388            *local += 1;
1389            (scope_id as u64) << 32 | id as u64
1390        } else {
1391            // Global sequential ID (for non-scoped views)
1392            let id = self.next_id;
1393            self.next_id += 1;
1394            id
1395        }
1396    }
1397
1398    pub fn id_count(&self) -> u64 {
1399        self.next_id - 1
1400    }
1401
1402    /// True while the named physical key is in the polled snapshot.
1403    pub fn is_held(&self, key: PhysicalKey) -> bool {
1404        self.held_keys.contains(&key)
1405    }
1406
1407    /// Snapshot the current ID counter (before executing a scope body) so the
1408    /// delta can be computed after the body returns.
1409    pub fn snapshot_id(&self) -> u64 {
1410        self.next_id
1411    }
1412
1413    /// Advance the ID counter by `count` without assigning IDs.
1414    /// Used by the scope! macro to reserve IDs for a cached scope subtree.
1415    pub fn advance_id(&mut self, count: u32) {
1416        self.next_id += count as u64;
1417    }
1418
1419    /// Number of IDs assigned since `prev_id` (the value returned by
1420    /// `snapshot_id()` before executing a scope body).
1421    pub fn ids_used_since(&self, prev_id: u64) -> u32 {
1422        (self.next_id - prev_id) as u32
1423    }
1424
1425    pub fn gc_scope_state(&mut self, live_keys: &rustc_hash::FxHashSet<String>) {
1426        self.scope_key_to_id
1427            .retain(|key, _| live_keys.contains(key));
1428        self.scope_local_counters
1429            .retain(|key, _| live_keys.contains(key));
1430    }
1431}
1432
1433/// RAII guard from [`Scheduler::scope_guard`]. Pops the scope on drop.
1434pub struct SchedulerScopeGuard<'a> {
1435    sched: &'a mut Scheduler,
1436}
1437
1438impl Drop for SchedulerScopeGuard<'_> {
1439    fn drop(&mut self) {
1440        self.sched.exit_scope();
1441    }
1442}
1443
1444/// Panic-safe scope guard that does NOT hold a borrow across the body.
1445///
1446/// The `scope!` macro uses this (not [`SchedulerScopeGuard`]) so the guarded
1447/// body — including nested `scope!` invocations — can keep using the
1448/// `Scheduler` normally. At drop time (normal or unwind) no other borrows of
1449/// the scheduler are live, since the body has ended.
1450pub struct SchedulerScopeGuardRaw {
1451    sched: *mut Scheduler,
1452}
1453
1454impl SchedulerScopeGuardRaw {
1455    /// # Safety
1456    /// `sched` must point to a valid `Scheduler` for the guard's lifetime,
1457    /// and the scheduler must not be used while the guard is being dropped
1458    /// (guaranteed when the guard outlives the body it protects).
1459    pub unsafe fn enter(sched: *mut Scheduler, key: &str) -> Self {
1460        unsafe {
1461            (*sched).enter_scope(key);
1462        }
1463        Self { sched }
1464    }
1465}
1466
1467impl Drop for SchedulerScopeGuardRaw {
1468    fn drop(&mut self) {
1469        unsafe {
1470            (*self.sched).exit_scope();
1471        }
1472    }
1473}
1474
1475impl Scheduler {
1476    pub fn repose<F>(
1477        &mut self,
1478        mut build_root: F,
1479        layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
1480    ) -> Frame
1481    where
1482        F: FnMut(&mut Scheduler) -> View,
1483    {
1484        let guard = ComposeGuard::begin();
1485        let root = guard.scope.run(|| build_root(self));
1486        let (scene, hits, sem) = layout_paint(&root, self.size);
1487
1488        let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
1489
1490        let live_scope_keys = guard.live_scope_keys();
1491        let frame = Frame {
1492            scene,
1493            hit_regions: hits,
1494            semantics_nodes: sem,
1495            focus_chain,
1496        };
1497        drop(guard);
1498        self.gc_scope_state(&live_scope_keys);
1499        frame
1500    }
1501}
1502
1503/// Test helper: full composer reset. Production shutdown should call
1504/// `shutdown_composition`.
1505#[cfg(test)]
1506pub fn clear_composer() {
1507    shutdown_composition();
1508}
1509
1510#[cfg(test)]
1511mod focus_trap_tests {
1512    use super::*;
1513
1514    fn region(id: u64, x: f32, group: Option<u64>) -> HitRegion {
1515        HitRegion {
1516            id,
1517            rect: Rect {
1518                x,
1519                y: 0.0,
1520                w: 10.0,
1521                h: 10.0,
1522            },
1523            focus_group_id: group,
1524            ..Default::default()
1525        }
1526    }
1527
1528    #[test]
1529    fn arrows_stay_inside_group() {
1530        // Dialog buttons 2,3 in group 9; background button 4 outside;
1531        // outsider 1 sits left of button 2 and would win unconstrained.
1532        let chain = vec![1, 2, 3, 4];
1533        let regions = vec![
1534            region(1, 0.0, None),
1535            region(2, 20.0, Some(9)),
1536            region(3, 40.0, Some(9)),
1537            region(4, 60.0, None),
1538        ];
1539        assert_eq!(
1540            spatial_focus_next(&chain, &regions, Some(2), FocusDirection::Left),
1541            None,
1542            "outsider 1 is left of 2 but outside the group: trapped"
1543        );
1544        assert_eq!(
1545            spatial_focus_next(&chain, &regions, Some(2), FocusDirection::Right),
1546            Some(3)
1547        );
1548        assert_eq!(
1549            spatial_focus_next(&chain, &regions, Some(3), FocusDirection::Left),
1550            Some(2)
1551        );
1552        assert_eq!(
1553            spatial_focus_next(&chain, &regions, Some(1), FocusDirection::Right),
1554            Some(2),
1555            "ungrouped focus still sees the full chain"
1556        );
1557    }
1558
1559    #[test]
1560    fn tab_cycles_inside_group() {
1561        let chain = vec![1, 2, 3, 4];
1562        let regions = vec![
1563            region(1, 0.0, None),
1564            region(2, 20.0, Some(9)),
1565            region(3, 40.0, Some(9)),
1566            region(4, 60.0, None),
1567        ];
1568        let mut fm = FocusManager::new(chain, Some(2));
1569        fm.hit_regions = regions;
1570        assert_eq!(fm.move_tab(false), Some(3));
1571        assert_eq!(fm.move_tab(false), Some(2));
1572        assert_eq!(fm.move_tab(true), Some(3));
1573    }
1574
1575    #[test]
1576    fn tab_from_outside_can_enter_group() {
1577        let chain = vec![1, 2, 3, 4];
1578        let regions = vec![
1579            region(1, 0.0, None),
1580            region(2, 20.0, Some(9)),
1581            region(3, 40.0, Some(9)),
1582            region(4, 60.0, None),
1583        ];
1584        let mut fm = FocusManager::new(chain, Some(1));
1585        fm.hit_regions = regions;
1586        assert_eq!(fm.move_tab(false), Some(2));
1587        let mut fm = FocusManager::new(vec![1, 2, 3, 4], Some(1));
1588        fm.hit_regions = vec![
1589            region(1, 0.0, None),
1590            region(2, 20.0, Some(9)),
1591            region(3, 40.0, Some(9)),
1592            region(4, 60.0, None),
1593        ];
1594        assert_eq!(fm.move_tab(true), Some(4));
1595    }
1596
1597    #[test]
1598    fn empty_group_never_moves() {
1599        let chain = vec![1, 4];
1600        let regions = vec![region(1, 0.0, None), region(4, 60.0, None)];
1601        let mut fm = FocusManager::new(chain, Some(1));
1602        fm.hit_regions = regions.clone();
1603        assert_eq!(fm.move_tab(false), Some(4));
1604        let chain = vec![1, 2, 4];
1605        let regions = vec![
1606            region(1, 0.0, None),
1607            region(2, 20.0, Some(77)),
1608            region(4, 60.0, None),
1609        ];
1610        let mut fm = FocusManager::new(chain, Some(2));
1611        fm.hit_regions = regions;
1612        assert_eq!(fm.move_tab(false), Some(2));
1613        assert_eq!(
1614            spatial_focus_next(&fm.chain, &fm.hit_regions, Some(2), FocusDirection::Right),
1615            None
1616        );
1617    }
1618}