Skip to main content

repose_core/
runtime.rs

1use std::any::Any;
2use std::cell::{Cell, RefCell};
3use std::panic::Location;
4use std::rc::Rc;
5
6use rustc_hash::FxHashMap;
7
8use crate::scope::Scope;
9use crate::{Rect, Scene, View, semantics::Role};
10
11thread_local! {
12    pub static COMPOSER: RefCell<Composer> = RefCell::new(Composer::default());
13    static ROOT_SCOPE: RefCell<Option<Scope>> = const { RefCell::new(None) };
14
15    /// A programmatic focus request, set by `FocusRequester::request_focus()` /
16    /// `free_focus()`. Stores the view ID that should receive focus on the next frame,
17    /// or `Some(CLEAR_FOCUS_MARKER)` to clear focus.
18    static FOCUS_REQUEST: Cell<Option<u64>> = const { Cell::new(None) };
19}
20
21/// Sentinel value meaning "clear focus entirely".
22pub const CLEAR_FOCUS_MARKER: u64 = u64::MAX;
23
24pub fn take_focus_request() -> Option<u64> {
25    FOCUS_REQUEST.with(|r| r.replace(None))
26}
27
28/// A handle that can programmatically request focus for a widget.
29///
30/// Similar to Compose's `FocusRequester`. Create one via `remember(FocusRequester::new)`,
31/// attach it via `.focus_requester(...)` on a modifier, and call `request_focus()` to
32/// move keyboard focus to the associated widget on the next frame.
33#[derive(Clone)]
34pub struct FocusRequester {
35    /// Target view ID, set during layout/paint by the modifier system.
36    pub target: Rc<RefCell<Option<u64>>>,
37}
38
39impl FocusRequester {
40    pub fn new() -> Self {
41        Self {
42            target: Rc::new(RefCell::new(None)),
43        }
44    }
45
46    /// Request focus for the associated widget on the next frame.
47    pub fn request_focus(&self) {
48        if let Some(id) = *self.target.borrow() {
49            FOCUS_REQUEST.with(|r| r.set(Some(id)));
50        }
51    }
52
53    /// Free/clear focus from the associated widget on the next frame.
54    /// If the associated widget currently has focus, focus is cleared entirely.
55    /// Corresponds to Compose's `freeFocus()`.
56    pub fn free_focus(&self) {
57        FOCUS_REQUEST.with(|r| r.set(Some(CLEAR_FOCUS_MARKER)));
58    }
59
60    /// Request focus for the associated widget on the next frame,
61    /// bypassing some focusability checks. Corresponds to Compose's
62    /// `captureFocus()`, which is typically used internally by the focus system.
63    /// In repose this is an alias for `request_focus()`.
64    pub fn capture_focus(&self) {
65        self.request_focus();
66    }
67}
68
69impl Default for FocusRequester {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75/// Direction for focus movement.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum FocusDirection {
78    Next,
79    Previous,
80    Left,
81    Right,
82    Up,
83    Down,
84}
85
86/// A manager for programmatic focus navigation.
87///
88/// Wraps a `&Scheduler` and provides methods to move focus.
89/// Can also be used standalone with a focus chain and focused element.
90#[derive(Clone)]
91pub struct FocusManager {
92    /// The ordered list of focusable element IDs.
93    pub chain: Vec<u64>,
94    /// The currently focused element (if any).
95    pub focused: Option<u64>,
96    /// Hit regions for focus group lookups.
97    pub hit_regions: Vec<HitRegion>,
98}
99
100impl FocusManager {
101    pub fn new(chain: Vec<u64>, focused: Option<u64>) -> Self {
102        Self {
103            chain,
104            focused,
105            hit_regions: Vec::new(),
106        }
107    }
108
109    /// Move focus in the given direction.
110    /// Returns the new focused element ID, or `None` if no movement is possible.
111    pub fn move_focus(&mut self, dir: FocusDirection) -> Option<u64> {
112        match dir {
113            FocusDirection::Next | FocusDirection::Previous => {
114                self.move_tab(dir == FocusDirection::Previous)
115            }
116            _ => None, // use move_focus_spatial when hit regions are available
117        }
118    }
119
120    /// Clears focus entirely on the next frame.
121    /// Corresponds to Compose's `FocusManager.clearFocus()`.
122    /// The `force` parameter is accepted for API compatibility; in repose
123    /// focus is always cleared immediately (no keep-focus mechanism).
124    pub fn clear_focus(&self, _force: bool) {
125        FOCUS_REQUEST.with(|r| r.set(Some(CLEAR_FOCUS_MARKER)));
126    }
127
128    /// Spatial focus navigation: find the closest focusable element in a given
129    /// direction using bounding rect geometry.
130    pub fn move_focus_spatial(
131        &mut self,
132        dir: FocusDirection,
133        hit_regions: &[HitRegion],
134    ) -> Option<u64> {
135        let next = spatial_focus_next(&self.chain, hit_regions, self.focused, dir)?;
136        self.focused = Some(next);
137        Some(next)
138    }
139
140    /// Tab forward or backward in the focus chain.
141    /// When the current focus belongs to a focus group, navigation is restricted
142    /// to elements within that group.
143    pub fn move_tab(&mut self, reverse: bool) -> Option<u64> {
144        if self.chain.is_empty() {
145            return None;
146        }
147        let current_group = self.focused.and_then(|cur| {
148            self.hit_regions
149                .iter()
150                .find(|h| h.id == cur)
151                .and_then(|h| h.focus_group_id)
152        });
153        let next = if let Some(group_id) = current_group {
154            // Build sub-chain of elements in the same focus group
155            let sub_chain: Vec<u64> = self
156                .chain
157                .iter()
158                .copied()
159                .filter(|&id| {
160                    id == group_id
161                        || self
162                            .hit_regions
163                            .iter()
164                            .any(|h| h.id == id && h.focus_group_id == Some(group_id))
165                })
166                .collect();
167            if sub_chain.is_empty() {
168                return None;
169            }
170            if let Some(cur) = self.focused {
171                if let Some(idx) = sub_chain.iter().position(|&id| id == cur) {
172                    if reverse {
173                        if idx == 0 {
174                            sub_chain[sub_chain.len() - 1]
175                        } else {
176                            sub_chain[idx - 1]
177                        }
178                    } else {
179                        sub_chain[(idx + 1) % sub_chain.len()]
180                    }
181                } else {
182                    sub_chain[0]
183                }
184            } else if reverse {
185                sub_chain[sub_chain.len() - 1]
186            } else {
187                sub_chain[0]
188            }
189        } else {
190            if let Some(cur) = self.focused {
191                if let Some(idx) = self.chain.iter().position(|&id| id == cur) {
192                    if reverse {
193                        if idx == 0 {
194                            self.chain[self.chain.len() - 1]
195                        } else {
196                            self.chain[idx - 1]
197                        }
198                    } else {
199                        self.chain[(idx + 1) % self.chain.len()]
200                    }
201                } else {
202                    self.chain[0]
203                }
204            } else if reverse {
205                self.chain[self.chain.len() - 1]
206            } else {
207                self.chain[0]
208            }
209        };
210        self.focused = Some(next);
211        Some(next)
212    }
213
214    /// Set target ID on a FocusRequester (called during layout).
215    pub fn set_requester_target(requester: &FocusRequester, id: u64) {
216        *requester.target.borrow_mut() = Some(id);
217    }
218}
219
220/// Find the next focusable element in a given spatial direction.
221///
222/// Uses the bounding rects from `hit_regions` to determine which element is
223/// "next" in the given direction from the currently focused element.
224pub fn spatial_focus_next(
225    chain: &[u64],
226    hit_regions: &[HitRegion],
227    current: Option<u64>,
228    dir: FocusDirection,
229) -> Option<u64> {
230    if chain.is_empty() {
231        return None;
232    }
233
234    let current_rect =
235        current.and_then(|id| hit_regions.iter().find(|h| h.id == id).map(|h| h.rect));
236
237    // For Next/Previous, use tab-order navigation
238    match dir {
239        FocusDirection::Next | FocusDirection::Previous => {
240            let mut fm = FocusManager {
241                chain: chain.to_vec(),
242                focused: current,
243                hit_regions: hit_regions.to_vec(),
244            };
245            return fm.move_tab(dir == FocusDirection::Previous);
246        }
247        _ => {}
248    }
249
250    let (cx, cy) = match current_rect {
251        Some(r) => (r.x + r.w / 2.0, r.y + r.h / 2.0),
252        None => {
253            // For games: Tab/Shift-Tab should establish initial focus, not arrows.
254            return None;
255        }
256    };
257
258    let mut best: Option<(u64, f32)> = None;
259
260    for &id in chain {
261        if Some(id) == current {
262            continue;
263        }
264        let Some(hr) = hit_regions.iter().find(|h| h.id == id) else {
265            continue;
266        };
267        let r = hr.rect;
268        let other_cx = r.x + r.w / 2.0;
269        let other_cy = r.y + r.h / 2.0;
270        let dx = other_cx - cx;
271        let dy = other_cy - cy;
272
273        let in_direction = match dir {
274            FocusDirection::Left => dx < 0.0 && dy.abs() <= r.h.max(1.0),
275            FocusDirection::Right => dx > 0.0 && dy.abs() <= r.h.max(1.0),
276            FocusDirection::Up => dy < 0.0 && dx.abs() <= r.w.max(1.0),
277            FocusDirection::Down => dy > 0.0 && dx.abs() <= r.w.max(1.0),
278            _ => false,
279        };
280
281        if !in_direction {
282            continue;
283        }
284
285        let dist = dx * dx + dy * dy;
286        let weight = dist / (r.w * r.h + 1.0).max(1.0);
287
288        match best {
289            Some((_, best_weight)) if weight >= best_weight => {}
290            _ => best = Some((id, weight)),
291        }
292    }
293
294    best.map(|(id, _)| id)
295}
296
297#[derive(Default)]
298pub struct Composer {
299    pub slots: Vec<Box<dyn Any>>,
300    /// Caller identity for each slot, used to detect stale slots
301    /// when the composition tree changes between frames.
302    pub slot_callers: Vec<&'static Location<'static>>,
303    pub cursor: usize,
304    pub keyed_slots: FxHashMap<String, Box<dyn Any>>,
305    /// Per-scope cached state for the `scope!` macro.
306    /// Keyed by the scope key string.
307    pub scope_caches: FxHashMap<String, crate::scope_cache::ScopeCache>,
308}
309
310pub struct ComposeGuard {
311    scope: Scope,
312}
313
314impl ComposeGuard {
315    pub fn begin() -> Self {
316        COMPOSER.with(|c| c.borrow_mut().cursor = 0);
317
318        let scope = ROOT_SCOPE.with(|rs| {
319            if let Some(existing) = rs.borrow().clone() {
320                existing
321            } else {
322                let s = Scope::new();
323                *rs.borrow_mut() = Some(s.clone());
324                s
325            }
326        });
327
328        ComposeGuard { scope }
329    }
330
331    pub fn scope(&self) -> &Scope {
332        &self.scope
333    }
334}
335
336impl Drop for ComposeGuard {
337    fn drop(&mut self) {
338        // ROOT_SCOPE.with(|rs| { Do not clear every frame
339        //     *rs.borrow_mut() = None;
340        // });
341    }
342}
343
344/// Slot-based remember (sequential composition only).
345/// This prevents state aliasing when the composition tree structure changes between frames
346#[track_caller]
347pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
348    // Capture BEFORE any closure -> Location::caller() returns the correct
349    // track_caller location only at the function's top level, not inside closures.
350    let caller = Location::caller();
351    COMPOSER.with(|c| {
352        let mut c = c.borrow_mut();
353        let cursor = c.cursor;
354        c.cursor += 1;
355
356        if cursor >= c.slots.len() {
357            let rc: Rc<T> = Rc::new(init());
358            c.slots.push(Box::new(rc.clone()));
359            c.slot_callers.push(caller);
360            return rc;
361        }
362
363        let stored_caller = c.slot_callers.get(cursor).copied();
364        if stored_caller != Some(caller) {
365            let rc: Rc<T> = Rc::new(init());
366            c.slots[cursor] = Box::new(rc.clone());
367            if cursor < c.slot_callers.len() {
368                c.slot_callers[cursor] = caller;
369            } else {
370                c.slot_callers.push(caller);
371            }
372            return rc;
373        }
374
375        if let Some(rc) = c.slots[cursor].downcast_ref::<Rc<T>>() {
376            rc.clone()
377        } else {
378            log::warn!(
379                "remember: slot {} type changed {}. \
380                 Use remember_with_key(key, || ...) for conditional branches.",
381                cursor,
382                std::any::type_name::<T>(),
383            );
384            let rc: Rc<T> = Rc::new(init());
385            c.slots[cursor] = Box::new(rc.clone());
386            rc
387        }
388    })
389}
390
391/// Key-based remember
392pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
393    COMPOSER.with(|c| {
394        let mut c = c.borrow_mut();
395        let key = key.into();
396
397        if let Some(existing) = c.keyed_slots.get(&key) {
398            if let Some(rc) = existing.downcast_ref::<Rc<T>>() {
399                return rc.clone();
400            } else {
401                log::warn!(
402                    "remember_with_key: key '{}' reused with a different type; replacing.",
403                    key
404                );
405            }
406        }
407
408        if cfg!(debug_assertions) && c.keyed_slots.len() > 10_000 {
409            log::warn!(
410                "remember_with_key: more than 10k keys stored; \
411                are you generating unbounded dynamic keys (e.g., using timestamps)?"
412            );
413        }
414
415        let rc: Rc<T> = Rc::new(init());
416        c.keyed_slots.insert(key, Box::new(rc.clone()));
417        rc
418    })
419}
420
421/// Raw slot state (`Rc<RefCell<T>>`). Writes via `borrow_mut()` don't request a
422/// frame - prefer [`remember_mutable`] if a write must always recompose.
423#[track_caller]
424pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
425    remember(|| RefCell::new(init()))
426}
427
428/// Key-based variant of [`remember_state`]; same no-frame-on-write caveat.
429pub fn remember_state_with_key<T: 'static>(
430    key: impl Into<String>,
431    init: impl FnOnce() -> T,
432) -> Rc<RefCell<T>> {
433    remember_with_key(key, || RefCell::new(init()))
434}
435
436/// Frame - output of composition for a tick: scene + input/semantics.
437#[derive(Clone)]
438pub struct Frame {
439    pub scene: Scene,
440    pub hit_regions: Vec<HitRegion>,
441    pub semantics_nodes: Vec<SemNode>,
442    pub focus_chain: Vec<u64>,
443}
444
445#[derive(Clone, Default)]
446pub struct HitRegion {
447    pub id: u64,
448    pub rect: Rect,
449    /// Tree depth: 0 = root, higher = deeper child. Used for three-pass
450    /// pointer dispatch to determine ancestor/descendant ordering.
451    pub depth: u32,
452    pub on_click: Option<Rc<dyn Fn()>>,
453    pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
454    pub focusable: bool,
455    pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
456    pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
457    pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
458    pub on_pointer_cancel: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
459    pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
460    pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
461    pub z_index: f32,
462    pub on_text_change: Option<Rc<dyn Fn(String)>>,
463    pub on_text_submit: Option<Rc<dyn Fn(String)>>,
464    /// If this hit region belongs to a TextField, this persistent key is used
465    /// for looking up platform-managed TextFieldState. Falls back to `id` if None.
466    pub tf_state_key: Option<u64>,
467
468    /// True if this hit region corresponds to a multiline text input (TextArea).
469    pub tf_multiline: bool,
470
471    /// Unclipped top-left of the TextField *content* box (padding-inset).
472    /// Used for pointer→grapheme mapping so parent scroll clipping of `rect`
473    /// does not shift selection into the top of the content.
474    /// `None` for non-textfields.
475    pub tf_content_origin: Option<(f32, f32)>,
476
477    // internal
478    pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
479    pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
480    pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
481    pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
482    pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
483    pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
484
485    pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
486
487    /// Called when a key event is received while this element is focused.
488    /// Return `true` to consume the event.
489    pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
490    /// Called before `on_key_event`. Return `true` to consume before normal dispatch.
491    pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
492
493    /// Cursor hint for desktop/web.
494    pub cursor: Option<crate::CursorIcon>,
495
496    /// If `Some(group_id)`, this hit region belongs to a focus group with the
497    /// given id. Tab navigation will cycle within the group instead of moving
498    /// to elements outside it. Set automatically by the layout engine when the
499    /// element is a descendant of a node with `focus_group: true`.
500    pub focus_group_id: Option<u64>,
501}
502
503impl HitRegion {
504    /// Seed a HitRegion with all the modifier's event handlers + dnd + cursor.
505    /// Call‑sites should only override the fields that differ (on_click, focusable, etc.)
506    /// via struct‑update syntax: `HitRegion { focusable: true, ..from_modifier(..) }`.
507    pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
508        Self {
509            id,
510            rect,
511            z_index: m.z_index,
512            on_click: m.on_click.clone(),
513            on_pointer_down: m.on_pointer_down.clone(),
514            on_pointer_move: m.on_pointer_move.clone(),
515            on_pointer_up: m.on_pointer_up.clone(),
516            on_pointer_cancel: m.on_pointer_cancel.clone(),
517            on_pointer_enter: m.on_pointer_enter.clone(),
518            on_pointer_leave: m.on_pointer_leave.clone(),
519            on_action: m.on_action.clone(),
520            on_key_event: m.on_key_event.clone(),
521            on_preview_key_event: m.on_preview_key_event.clone(),
522            cursor: m.cursor,
523            on_drag_start: m.on_drag_start.clone(),
524            on_drag_end: m.on_drag_end.clone(),
525            on_drag_enter: m.on_drag_enter.clone(),
526            on_drag_over: m.on_drag_over.clone(),
527            on_drag_leave: m.on_drag_leave.clone(),
528            on_scroll: m.on_scroll.clone(),
529            on_drop: m.on_drop.clone(),
530            ..Default::default()
531        }
532    }
533}
534
535/// Flattened semantics node produced by `layout_and_paint`.
536///
537/// This is the source of truth for accessibility backends: it contains the
538/// resolved screen rect, role, label, and focus/enabled state.
539///
540/// The platform runner should convert this into OS‑specific accessibility trees (when implemented)
541/// (AT‑SPI on Linux, TalkBack on Android, etc.).
542#[derive(Clone)]
543pub struct SemNode {
544    /// Stable id, shared with the associated `HitRegion` / `ViewId`.
545    pub id: u64,
546
547    /// `None` means direct child of the window root.
548    pub parent: Option<u64>,
549
550    pub role: Role,
551    pub label: Option<String>,
552    pub rect: Rect,
553    pub focused: bool,
554    pub enabled: bool,
555    /// Marks this node as a collection of selectable children (e.g., Tabs).
556    pub selectable_group: bool,
557}
558
559pub struct Scheduler {
560    next_id: u64,
561    /// Per-scope unique IDs, assigned lazily when a scope first executes.
562    /// Keyed by the scope key string from `scope!`.
563    scope_key_to_id: FxHashMap<String, u32>,
564    next_scope_id: u32,
565    /// When set, `id()` allocates from this scope's local counter instead of the global counter.
566    /// The returned ID is `(scope_id << 32) | local_id`, which is stable even when
567    /// prior sibling scopes change their view count.
568    current_scope: Option<String>,
569    /// Per-scope local ID counters. Reset to 0 when a scope re-executes.
570    scope_local_counters: FxHashMap<String, u32>,
571    pub focused: Option<u64>,
572    pub size: (u32, u32),
573}
574
575impl Default for Scheduler {
576    fn default() -> Self {
577        Self::new()
578    }
579}
580
581impl Scheduler {
582    pub fn new() -> Self {
583        Self {
584            next_id: 1,
585            scope_key_to_id: FxHashMap::default(),
586            next_scope_id: 1,
587            current_scope: None,
588            scope_local_counters: FxHashMap::default(),
589            focused: None,
590            size: (1280, 800),
591        }
592    }
593
594    /// Enter a named scope. Subsequent `id()` calls within this scope
595    /// will allocate from the scope's local counter, producing packed
596    /// `(scope_id << 32) | local_id` values that are stable across sibling
597    /// recompositions.
598    pub fn enter_scope(&mut self, key: &str) {
599        self.current_scope = Some(key.to_string());
600        // Reset local counter -> the body will re-assign IDs fresh
601        self.scope_local_counters.insert(key.to_string(), 0);
602        // Ensure a scope_id exists (lazy allocation)
603        self.get_or_create_scope_id(key);
604    }
605
606    /// Exit the current scope. Subsequent `id()` calls return global IDs again.
607    pub fn exit_scope(&mut self) {
608        self.current_scope = None;
609    }
610
611    fn get_or_create_scope_id(&mut self, key: &str) -> u32 {
612        if let Some(&id) = self.scope_key_to_id.get(key) {
613            id
614        } else {
615            let id = self.next_scope_id;
616            self.next_scope_id += 1;
617            self.scope_key_to_id.insert(key.to_string(), id);
618            id
619        }
620    }
621
622    pub fn id(&mut self) -> u64 {
623        if let Some(key) = &self.current_scope {
624            // Scope-local ID: packed (scope_id << 32) | local_id
625            let scope_id = self.scope_key_to_id.get(key).copied().unwrap_or(0);
626            let local = self.scope_local_counters.get_mut(key).unwrap();
627            let id = *local;
628            *local += 1;
629            (scope_id as u64) << 32 | id as u64
630        } else {
631            // Global sequential ID (for non-scoped views)
632            let id = self.next_id;
633            self.next_id += 1;
634            id
635        }
636    }
637
638    pub fn id_count(&self) -> u64 {
639        self.next_id - 1
640    }
641
642    /// Snapshot the current ID counter (before executing a scope body) so the
643    /// delta can be computed after the body returns.
644    pub fn snapshot_id(&self) -> u64 {
645        self.next_id
646    }
647
648    /// Advance the ID counter by `count` without assigning IDs.
649    /// Used by the scope! macro to reserve IDs for a cached scope subtree.
650    pub fn advance_id(&mut self, count: u32) {
651        self.next_id += count as u64;
652    }
653
654    /// Number of IDs assigned since `prev_id` (the value returned by
655    /// `snapshot_id()` before executing a scope body).
656    pub fn ids_used_since(&self, prev_id: u64) -> u32 {
657        (self.next_id - prev_id) as u32
658    }
659
660    pub fn repose<F>(
661        &mut self,
662        mut build_root: F,
663        layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
664    ) -> Frame
665    where
666        F: FnMut(&mut Scheduler) -> View,
667    {
668        let guard = ComposeGuard::begin();
669        let root = guard.scope.run(|| build_root(self));
670        let (scene, hits, sem) = layout_paint(&root, self.size);
671
672        let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
673
674        Frame {
675            scene,
676            hit_regions: hits,
677            semantics_nodes: sem,
678            focus_chain,
679        }
680    }
681}
682
683/// Avoids cross-test pollution
684#[cfg(test)]
685pub fn clear_composer() {
686    COMPOSER.with(|c| {
687        let mut c = c.borrow_mut();
688        c.slots.clear();
689        c.slot_callers.clear();
690        c.keyed_slots.clear();
691        c.scope_caches.clear();
692        c.cursor = 0;
693    });
694    ROOT_SCOPE.with(|rs| {
695        *rs.borrow_mut() = None;
696    });
697}