Skip to main content

repose_core/
runtime.rs

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