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    /// 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    let has_wrong_type = COMPOSER.with(|composer| {
1040        let composer = composer.borrow();
1041        if composer.keyed_slots.contains_key(&key) {
1042            log::warn!(
1043                "remember_with_key: key '{}' reused with a different type; replacing.",
1044                key
1045            );
1046        }
1047        cfg!(debug_assertions) && composer.keyed_slots.len() > 10_000
1048    });
1049    if has_wrong_type {
1050        log::warn!(
1051            "remember_with_key: more than 10k keys stored; are you generating unbounded dynamic keys?"
1052        );
1053    }
1054
1055    let mut initializer = InitializerGuard::enter_keyed();
1056    let value = Rc::new(init());
1057    let (result, old) = COMPOSER.with(|composer| {
1058        let mut composer = composer.borrow_mut();
1059        if let Some(existing) = composer.keyed_slots.get(&key)
1060            && let Some(existing) = existing.downcast_ref::<Rc<T>>()
1061        {
1062            let existing = existing.clone();
1063            composer.live_keyed_keys.insert(key.clone());
1064            composer.keyed_owner.insert(key.clone(), owner.clone());
1065            composer
1066                .keyed_owners
1067                .entry(key.clone())
1068                .or_default()
1069                .insert(owner.clone());
1070            composer.live_keyed_owners.insert(owner);
1071            return (existing, None);
1072        }
1073        let old = composer
1074            .keyed_slots
1075            .insert(key.clone(), Box::new(value.clone()));
1076        composer.live_keyed_keys.insert(key.clone());
1077        composer.keyed_owner.insert(key.clone(), owner.clone());
1078        composer
1079            .keyed_owners
1080            .entry(key)
1081            .or_default()
1082            .insert(owner.clone());
1083        (value, old)
1084    });
1085    initializer.commit();
1086    drop(old);
1087    result
1088}
1089
1090/// Raw slot state (`Rc<RefCell<T>>`). Writes via `borrow_mut()` don't request a
1091/// frame - prefer [`remember_mutable`] if a write must always recompose.
1092#[track_caller]
1093pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
1094    remember(|| RefCell::new(init()))
1095}
1096
1097/// Key-based variant of [`remember_state`]. Same no-frame-on-write caveat.
1098pub fn remember_state_with_key<T: 'static>(
1099    key: impl Into<String>,
1100    init: impl FnOnce() -> T,
1101) -> Rc<RefCell<T>> {
1102    remember_with_key(key, || RefCell::new(init()))
1103}
1104
1105/// Auto-keyed variant of [`remember_state`]. Same positional caveat as
1106/// [`remember_auto`] and the same no-frame-on-write caveat as [`remember_state`].
1107#[track_caller]
1108pub fn remember_state_auto<T: 'static>(slot: &str, init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
1109    remember_auto(slot, || RefCell::new(init()))
1110}
1111
1112/// Frame - output of composition for a tick: scene + input/semantics.
1113#[derive(Clone)]
1114pub struct Frame {
1115    pub scene: Scene,
1116    pub hit_regions: Vec<HitRegion>,
1117    pub semantics_nodes: Vec<SemNode>,
1118    pub focus_chain: Vec<u64>,
1119}
1120
1121/// Hit-test region in physical pixels (`rect` carries px magnitudes,
1122/// like Compose `Rect`).
1123#[derive(Clone, Default)]
1124pub struct HitRegion {
1125    pub id: u64,
1126    pub rect: Rect,
1127    /// Tree depth: 0 = root, higher = deeper child. Used for three-pass
1128    /// pointer dispatch to determine ancestor/descendant ordering.
1129    pub depth: u32,
1130    pub parent: Option<u64>,
1131    pub on_click: Option<Rc<dyn Fn()>>,
1132    pub on_double_click: Option<Rc<dyn Fn()>>,
1133    pub on_long_click: Option<Rc<dyn Fn()>>,
1134    pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
1135    pub focusable: bool,
1136    pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1137    pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1138    pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1139    pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1140    pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1141    pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
1142    pub z_index: f32,
1143    pub disabled: bool,
1144    pub on_text_change: Option<Rc<dyn Fn(String)>>,
1145    pub on_text_submit: Option<Rc<dyn Fn(String)>>,
1146    /// If this hit region belongs to a TextField, this persistent key is used
1147    /// for looking up platform-managed TextFieldState. Falls back to `id` if None.
1148    pub tf_state_key: Option<u64>,
1149
1150    /// True if this hit region corresponds to a multiline text input (TextArea).
1151    pub tf_multiline: bool,
1152
1153    /// Unclipped top-left of the TextField *content* box (padding-inset).
1154    /// Used for pointer->grapheme mapping so parent scroll clipping of `rect`
1155    /// does not shift selection into the top of the content.
1156    /// `None` for non-textfields.
1157    pub tf_content_origin: Option<(f32, f32)>,
1158
1159    /// When false, the field rejects edits and is not focusable
1160    pub tf_enabled: bool,
1161
1162    /// When true, selection/focus/copy are allowed but mutations are rejected
1163    pub tf_read_only: bool,
1164
1165    pub tf_sensitive: bool,
1166
1167    /// Controlled text snapshot for this field (last compose).
1168    pub tf_value: String,
1169
1170    /// Font size for this text field in [`Sp`](crate::units::Sp)
1171    /// (for hit-test / caret mapping). `Sp::ZERO` means use `TF_FONT_SP` default.
1172    pub tf_font_size: crate::units::Sp,
1173
1174    // internal
1175    pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
1176    pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
1177    pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1178    pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1179    pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
1180    pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
1181    /// Copied onto the drag session when a drag starts from this region.
1182    pub drag_preview: Option<crate::dnd::DragPreview>,
1183
1184    pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
1185
1186    /// Called when a key event is received while this element is focused.
1187    /// Return `true` to consume the event.
1188    pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
1189    /// Called before `on_key_event`. Return `true` to consume before normal dispatch.
1190    pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
1191
1192    /// Cursor hint for desktop/web.
1193    pub cursor: Option<crate::CursorIcon>,
1194
1195    /// If `Some(group_id)`, this hit region belongs to a focus group with the
1196    /// given id. Tab navigation will cycle within the group instead of moving
1197    /// to elements outside it. Set automatically by the layout engine when the
1198    /// element is a descendant of a node with `focus_group: true`.
1199    pub focus_group_id: Option<u64>,
1200
1201    /// IME keyboard hints, populated for text-field hit regions so the
1202    /// platform runner can configure the OS keyboard / IME on focus.
1203    pub keyboard_type: crate::text::KeyboardType,
1204    pub capitalization: crate::text::KeyboardCapitalization,
1205    pub ime_action: crate::text::ImeAction,
1206    /// Whether auto-correct is enabled. `None` = follow platform default;
1207    /// password keyboards always resolve to `false` in the layout engine.
1208    pub auto_correct: Option<bool>,
1209
1210    /// Shared interaction source auto-wired by the layout engine
1211    /// (press/hover/focus/drag). Used by keyboard activation and focus
1212    /// transitions so they stay in parity with pointer input.
1213    /// `None` when the component does not need one (no indication/state colors).
1214    pub interaction_source: Option<crate::modifier::InteractionSource>,
1215}
1216
1217impl HitRegion {
1218    /// Seed a HitRegion with all the modifier's event handlers + dnd + cursor.
1219    /// Call‑sites should only override the fields that differ (on_click, focusable, etc.)
1220    /// via struct‑update syntax: `HitRegion { focusable: true, ..from_modifier(..) }`.
1221    pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
1222        Self {
1223            id,
1224            rect,
1225            z_index: m.z_index,
1226            on_click: m.on_click.clone(),
1227            on_double_click: m.on_double_click.clone(),
1228            on_long_click: m.on_long_click.clone(),
1229            on_pointer_down: m.on_pointer_down.clone(),
1230            on_pointer_move: m.on_pointer_move.clone(),
1231            on_pointer_up: m.on_pointer_up.clone(),
1232            on_pointer_cancel: m.on_pointer_cancel.clone(),
1233            on_pointer_enter: m.on_pointer_enter.clone(),
1234            on_pointer_leave: m.on_pointer_leave.clone(),
1235            on_action: m.on_action.clone(),
1236            on_key_event: m.on_key_event.clone(),
1237            on_preview_key_event: m.on_preview_key_event.clone(),
1238            cursor: m.cursor.clone(),
1239            on_drag_start: m.on_drag_start.clone(),
1240            on_drag_end: m.on_drag_end.clone(),
1241            on_drag_enter: m.on_drag_enter.clone(),
1242            on_drag_over: m.on_drag_over.clone(),
1243            on_drag_leave: m.on_drag_leave.clone(),
1244            on_scroll: m.on_scroll.clone(),
1245            on_drop: m.on_drop.clone(),
1246            drag_preview: m.drag_preview.clone(),
1247            disabled: m.disabled,
1248            tf_enabled: true,
1249            tf_read_only: false,
1250            ..Default::default()
1251        }
1252    }
1253}
1254
1255/// Flattened semantics node produced by `layout_and_paint`.
1256///
1257/// This is the source of truth for accessibility backends: it contains the
1258/// resolved screen rect, role, label, and focus/enabled state.
1259///
1260/// The platform runner should convert this into OS‑specific accessibility trees (when implemented)
1261/// (AT‑SPI on Linux, TalkBack on Android, etc.).
1262#[derive(Clone)]
1263pub struct SemNode {
1264    /// Stable id, shared with the associated `HitRegion` / `ViewId`.
1265    pub id: u64,
1266
1267    /// `None` means direct child of the window root.
1268    pub parent: Option<u64>,
1269
1270    pub role: Role,
1271    pub label: Option<String>,
1272    pub rect: Rect,
1273    pub focused: bool,
1274    pub enabled: bool,
1275    /// Marks this node as a collection of selectable children (e.g., Tabs).
1276    pub selectable_group: bool,
1277    pub checked: Option<bool>,
1278    pub selected: Option<bool>,
1279    pub value: Option<String>,
1280}
1281
1282impl Default for SemNode {
1283    fn default() -> Self {
1284        Self {
1285            id: 0,
1286            parent: None,
1287            role: Role::default(),
1288            label: None,
1289            rect: Rect::default(),
1290            focused: false,
1291            enabled: true,
1292            selectable_group: false,
1293            checked: None,
1294            selected: None,
1295            value: None,
1296        }
1297    }
1298}
1299
1300pub struct Scheduler {
1301    next_id: u64,
1302    /// Per-scope unique IDs, assigned lazily when a scope first executes.
1303    /// Keyed by the scope key string from `scope!`.
1304    scope_key_to_id: FxHashMap<String, u32>,
1305    next_scope_id: u32,
1306    /// Stack of active scope keys. `id()` allocates from the innermost scope
1307    /// so nested `scope!` bodies get stable packed IDs and the outer scope
1308    /// resumes correctly after the inner exits (previously `exit_scope` reset
1309    /// to `None`, leaking outer IDs into the global sequence).
1310    current_scope: Vec<String>,
1311    /// Per-scope local ID counters. Reset to 0 when a scope re-executes.
1312    scope_local_counters: FxHashMap<String, u32>,
1313    pub focused: Option<u64>,
1314    pub size: (u32, u32),
1315    /// Last known mouse pointer position in physical px, updated on
1316    /// mouse move/press/release without passing through focus dispatch.
1317    pub pointer_pos_px: Option<(f32, f32)>,
1318    /// Polled physical keys currently down, layout-independent
1319    /// positions (see [`PhysicalKey`]). The platform runner maintains
1320    /// this from raw `KeyboardInput` events without passing through
1321    /// focus dispatch; games reconcile their event-staged held sets
1322    /// against it every frame (GML `keyboard_check` parity). Cleared
1323    /// on window focus loss (no key-ups arrive across an alt-tab).
1324    pub held_keys: HashSet<PhysicalKey>,
1325    /// Window focus as of the last platform event. `false` drops every
1326    /// held key on the game side.
1327    pub window_focused: bool,
1328    /// Live touch contacts in physical px: `(touch id, x, y)` per
1329    /// finger currently down. Written by the platform runners from
1330    /// raw `WindowEvent::Touch` (Started/Moved/Ended-Cancelled), read
1331    /// by games through `feed_polled`-style staging snapshots
1332    /// (GML `device_mouse_x_to_gui(i)` parity: stable per-finger slot
1333    /// ids, positions sampled every tick while held).
1334    pub touch_points: Vec<(u64, f32, f32)>,
1335    /// Polled mouse-button levels, same source as `held_keys`
1336    /// (GML `mouse_check_button` parity; repairs a missed button-up
1337    /// when the release lands outside the window).
1338    pub mouse_primary: bool,
1339    pub mouse_secondary: bool,
1340    pub mouse_middle: bool,
1341    /// App-requested cursor override. When `Some`, the platform
1342    /// applies it instead of the hover-derived icon: games hide the
1343    /// OS pointer here (`CursorIcon::Hidden`) while drawing their own
1344    /// crosshair, GML `window_set_cursor(cr_none)` parity. `None`
1345    /// restores hover behavior. Set during composition (any view can
1346    /// write it; last write per frame wins), consumed by the runner
1347    /// after `frame()`.
1348    pub cursor_override: Option<CursorIcon>,
1349}
1350
1351impl Default for Scheduler {
1352    fn default() -> Self {
1353        Self::new()
1354    }
1355}
1356
1357impl Scheduler {
1358    pub fn new() -> Self {
1359        Self {
1360            next_id: 1,
1361            scope_key_to_id: FxHashMap::default(),
1362            next_scope_id: 1,
1363            current_scope: Vec::new(),
1364            scope_local_counters: FxHashMap::default(),
1365            focused: None,
1366            size: (1280, 800),
1367            pointer_pos_px: None,
1368            held_keys: HashSet::new(),
1369            window_focused: true,
1370            touch_points: Vec::new(),
1371            mouse_primary: false,
1372            mouse_secondary: false,
1373            mouse_middle: false,
1374            cursor_override: None,
1375        }
1376    }
1377
1378    /// Enter a named scope. Subsequent `id()` calls within this scope
1379    /// will allocate from the scope's local counter, producing packed
1380    /// `(scope_id << 32) | local_id` values that are stable across sibling
1381    /// recompositions. Nested scopes push; `exit_scope` pops.
1382    pub fn enter_scope(&mut self, key: &str) {
1383        if !self.current_scope.iter().any(|k| k == key) {
1384            self.scope_local_counters.insert(key.to_string(), 0);
1385        }
1386        self.current_scope.push(key.to_string());
1387        self.get_or_create_scope_id(key);
1388    }
1389
1390    /// Exit the innermost scope. No-op if the stack is empty or the top does
1391    /// not match (defensive: never corrupt an outer scope).
1392    pub fn exit_scope(&mut self) {
1393        self.current_scope.pop();
1394    }
1395
1396    /// RAII scope entry: pops on drop, so a panicking scope body cannot leave
1397    /// the scheduler stuck in the wrong scope (which previously leaked outer
1398    /// IDs into the global sequence).
1399    pub fn scope_guard<'a>(&'a mut self, key: &str) -> SchedulerScopeGuard<'a> {
1400        self.enter_scope(key);
1401        SchedulerScopeGuard { sched: self }
1402    }
1403
1404    /// Panic-safe scope entry that does NOT hold a borrow: the guarded body
1405    /// (including nested `scope!`) can keep using the `Scheduler`. Used by
1406    /// the `scope!` macro.
1407    pub fn scope_guard_raw(&mut self, key: &str) -> SchedulerScopeGuardRaw {
1408        let ptr = self as *mut Scheduler;
1409        unsafe { SchedulerScopeGuardRaw::enter(ptr, key) }
1410    }
1411
1412    fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
1413        if let Some(&id) = self.scope_key_to_id.get(key) {
1414            id
1415        } else {
1416            let id = self.next_scope_id;
1417            self.next_scope_id += 1;
1418            self.scope_key_to_id.insert(key.to_string(), id);
1419            id
1420        }
1421    }
1422
1423    pub fn id(&mut self) -> u64 {
1424        if let Some(key) = self.current_scope.last().cloned() {
1425            let scope_id = self.scope_key_to_id.get(&key).copied().unwrap_or(0);
1426            let local = self.scope_local_counters.get_mut(&key).unwrap();
1427            let id = *local;
1428            *local += 1;
1429            (scope_id as u64) << 32 | id as u64
1430        } else {
1431            // Global sequential ID (for non-scoped views)
1432            let id = self.next_id;
1433            self.next_id += 1;
1434            id
1435        }
1436    }
1437
1438    pub fn id_count(&self) -> u64 {
1439        self.next_id - 1
1440    }
1441
1442    /// True while the named physical key is in the polled snapshot.
1443    pub fn is_held(&self, key: PhysicalKey) -> bool {
1444        self.held_keys.contains(&key)
1445    }
1446
1447    /// Snapshot the current ID counter (before executing a scope body) so the
1448    /// delta can be computed after the body returns.
1449    pub fn snapshot_id(&self) -> u64 {
1450        self.next_id
1451    }
1452
1453    /// Advance the ID counter by `count` without assigning IDs.
1454    /// Used by the scope! macro to reserve IDs for a cached scope subtree.
1455    pub fn advance_id(&mut self, count: u32) {
1456        self.next_id += count as u64;
1457    }
1458
1459    /// Number of IDs assigned since `prev_id` (the value returned by
1460    /// `snapshot_id()` before executing a scope body).
1461    pub fn ids_used_since(&self, prev_id: u64) -> u32 {
1462        (self.next_id - prev_id) as u32
1463    }
1464
1465    pub fn gc_scope_state(&mut self, live_keys: &rustc_hash::FxHashSet<String>) {
1466        self.scope_key_to_id
1467            .retain(|key, _| live_keys.contains(key));
1468        self.scope_local_counters
1469            .retain(|key, _| live_keys.contains(key));
1470    }
1471}
1472
1473/// RAII guard from [`Scheduler::scope_guard`]. Pops the scope on drop.
1474pub struct SchedulerScopeGuard<'a> {
1475    sched: &'a mut Scheduler,
1476}
1477
1478impl Drop for SchedulerScopeGuard<'_> {
1479    fn drop(&mut self) {
1480        self.sched.exit_scope();
1481    }
1482}
1483
1484/// Panic-safe scope guard that does NOT hold a borrow across the body.
1485///
1486/// The `scope!` macro uses this (not [`SchedulerScopeGuard`]) so the guarded
1487/// body — including nested `scope!` invocations — can keep using the
1488/// `Scheduler` normally. At drop time (normal or unwind) no other borrows of
1489/// the scheduler are live, since the body has ended.
1490pub struct SchedulerScopeGuardRaw {
1491    sched: *mut Scheduler,
1492}
1493
1494impl SchedulerScopeGuardRaw {
1495    /// # Safety
1496    /// `sched` must point to a valid `Scheduler` for the guard's lifetime,
1497    /// and the scheduler must not be used while the guard is being dropped
1498    /// (guaranteed when the guard outlives the body it protects).
1499    pub unsafe fn enter(sched: *mut Scheduler, key: &str) -> Self {
1500        unsafe {
1501            (*sched).enter_scope(key);
1502        }
1503        Self { sched }
1504    }
1505}
1506
1507impl Drop for SchedulerScopeGuardRaw {
1508    fn drop(&mut self) {
1509        unsafe {
1510            (*self.sched).exit_scope();
1511        }
1512    }
1513}
1514
1515impl Scheduler {
1516    pub fn repose<F>(
1517        &mut self,
1518        mut build_root: F,
1519        layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
1520    ) -> Frame
1521    where
1522        F: FnMut(&mut Scheduler) -> View,
1523    {
1524        let guard = ComposeGuard::begin();
1525        let root = guard.scope.run(|| build_root(self));
1526        let (scene, hits, sem) = layout_paint(&root, self.size);
1527
1528        let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
1529
1530        let live_scope_keys = guard.live_scope_keys();
1531        let frame = Frame {
1532            scene,
1533            hit_regions: hits,
1534            semantics_nodes: sem,
1535            focus_chain,
1536        };
1537        drop(guard);
1538        self.gc_scope_state(&live_scope_keys);
1539        frame
1540    }
1541}
1542
1543/// Test helper: full composer reset. Production shutdown should call
1544/// `shutdown_composition`.
1545#[cfg(test)]
1546pub fn clear_composer() {
1547    shutdown_composition();
1548}
1549
1550#[cfg(test)]
1551mod focus_trap_tests {
1552    use super::*;
1553
1554    fn region(id: u64, x: f32, group: Option<u64>) -> HitRegion {
1555        HitRegion {
1556            id,
1557            rect: Rect {
1558                x,
1559                y: 0.0,
1560                w: 10.0,
1561                h: 10.0,
1562            },
1563            focus_group_id: group,
1564            ..Default::default()
1565        }
1566    }
1567
1568    #[test]
1569    fn arrows_stay_inside_group() {
1570        // Dialog buttons 2,3 in group 9; background button 4 outside;
1571        // outsider 1 sits left of button 2 and would win unconstrained.
1572        let chain = vec![1, 2, 3, 4];
1573        let regions = vec![
1574            region(1, 0.0, None),
1575            region(2, 20.0, Some(9)),
1576            region(3, 40.0, Some(9)),
1577            region(4, 60.0, None),
1578        ];
1579        assert_eq!(
1580            spatial_focus_next(&chain, &regions, Some(2), FocusDirection::Left),
1581            None,
1582            "outsider 1 is left of 2 but outside the group: trapped"
1583        );
1584        assert_eq!(
1585            spatial_focus_next(&chain, &regions, Some(2), FocusDirection::Right),
1586            Some(3)
1587        );
1588        assert_eq!(
1589            spatial_focus_next(&chain, &regions, Some(3), FocusDirection::Left),
1590            Some(2)
1591        );
1592        assert_eq!(
1593            spatial_focus_next(&chain, &regions, Some(1), FocusDirection::Right),
1594            Some(2),
1595            "ungrouped focus still sees the full chain"
1596        );
1597    }
1598
1599    #[test]
1600    fn tab_cycles_inside_group() {
1601        let chain = vec![1, 2, 3, 4];
1602        let regions = vec![
1603            region(1, 0.0, None),
1604            region(2, 20.0, Some(9)),
1605            region(3, 40.0, Some(9)),
1606            region(4, 60.0, None),
1607        ];
1608        let mut fm = FocusManager::new(chain, Some(2));
1609        fm.hit_regions = regions;
1610        assert_eq!(fm.move_tab(false), Some(3));
1611        assert_eq!(fm.move_tab(false), Some(2));
1612        assert_eq!(fm.move_tab(true), Some(3));
1613    }
1614
1615    #[test]
1616    fn tab_from_outside_can_enter_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(1));
1625        fm.hit_regions = regions;
1626        assert_eq!(fm.move_tab(false), Some(2));
1627        let mut fm = FocusManager::new(vec![1, 2, 3, 4], Some(1));
1628        fm.hit_regions = vec![
1629            region(1, 0.0, None),
1630            region(2, 20.0, Some(9)),
1631            region(3, 40.0, Some(9)),
1632            region(4, 60.0, None),
1633        ];
1634        assert_eq!(fm.move_tab(true), Some(4));
1635    }
1636
1637    #[test]
1638    fn empty_group_never_moves() {
1639        let chain = vec![1, 4];
1640        let regions = vec![region(1, 0.0, None), region(4, 60.0, None)];
1641        let mut fm = FocusManager::new(chain, Some(1));
1642        fm.hit_regions = regions.clone();
1643        assert_eq!(fm.move_tab(false), Some(4));
1644        let chain = vec![1, 2, 4];
1645        let regions = vec![
1646            region(1, 0.0, None),
1647            region(2, 20.0, Some(77)),
1648            region(4, 60.0, None),
1649        ];
1650        let mut fm = FocusManager::new(chain, Some(2));
1651        fm.hit_regions = regions;
1652        assert_eq!(fm.move_tab(false), Some(2));
1653        assert_eq!(
1654            spatial_focus_next(&fm.chain, &fm.hit_regions, Some(2), FocusDirection::Right),
1655            None
1656        );
1657    }
1658}