repose_core/
runtime.rs

1use std::any::Any;
2use std::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>> = RefCell::new(None);
12}
13
14#[derive(Default)]
15pub struct Composer {
16    pub slots: Vec<Box<dyn Any>>,
17    pub cursor: usize,
18    pub keyed_slots: HashMap<String, Box<dyn Any>>,
19}
20
21pub struct ComposeGuard {
22    scope: Scope,
23}
24
25impl ComposeGuard {
26    pub fn begin() -> Self {
27        let scope = Scope::new();
28
29        COMPOSER.with(|c| {
30            let mut c = c.borrow_mut();
31            c.cursor = 0;
32        });
33
34        ROOT_SCOPE.with(|rs| {
35            *rs.borrow_mut() = Some(scope.clone());
36        });
37
38        ComposeGuard { scope }
39    }
40
41    pub fn scope(&self) -> &Scope {
42        &self.scope
43    }
44}
45
46impl Drop for ComposeGuard {
47    fn drop(&mut self) {
48        ROOT_SCOPE.with(|rs| {
49            *rs.borrow_mut() = None;
50        });
51    }
52}
53
54/// Slot-based remember (sequential composition only)
55pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
56    COMPOSER.with(|c| {
57        let mut c = c.borrow_mut();
58        let cursor = c.cursor;
59        c.cursor += 1;
60
61        if cursor >= c.slots.len() {
62            let rc: Rc<T> = Rc::new(init());
63            c.slots.push(Box::new(rc.clone()));
64            return rc;
65        }
66
67        if let Some(rc) = c.slots[cursor].downcast_ref::<Rc<T>>() {
68            rc.clone()
69        } else {
70            // replace (else panics)
71            log::warn!(
72                "remember: slot {} type changed; replacing. \
73                 If this is due to conditional composition, prefer remember_with_key.",
74                cursor
75            );
76            let rc: Rc<T> = Rc::new(init());
77            c.slots[cursor] = Box::new(rc.clone());
78            rc
79        }
80    })
81}
82
83/// Key-based remember
84pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
85    COMPOSER.with(|c| {
86        let mut c = c.borrow_mut();
87        let key = key.into();
88
89        if let Some(existing) = c.keyed_slots.get(&key) {
90            if let Some(rc) = existing.downcast_ref::<Rc<T>>() {
91                return rc.clone();
92            } else {
93                log::warn!(
94                    "remember_with_key: key '{}' reused with a different type; replacing.",
95                    key
96                );
97            }
98        }
99
100        if cfg!(debug_assertions) && c.keyed_slots.len() > 10_000 {
101            log::warn!(
102                "remember_with_key: more than 10k keys stored; \
103                are you generating unbounded dynamic keys (e.g., using timestamps)?"
104            );
105        }
106
107        let rc: Rc<T> = Rc::new(init());
108        c.keyed_slots.insert(key, Box::new(rc.clone()));
109        rc
110    })
111}
112
113pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
114    remember(|| RefCell::new(init()))
115}
116
117pub fn remember_state_with_key<T: 'static>(
118    key: impl Into<String>,
119    init: impl FnOnce() -> T,
120) -> Rc<RefCell<T>> {
121    remember_with_key(key, || RefCell::new(init()))
122}
123
124/// Frame — output of composition for a tick: scene + input/semantics.
125pub struct Frame {
126    pub scene: Scene,
127    pub hit_regions: Vec<HitRegion>,
128    pub semantics_nodes: Vec<SemNode>,
129    pub focus_chain: Vec<u64>,
130}
131
132#[derive(Clone)]
133pub struct HitRegion {
134    pub id: u64,
135    pub rect: Rect,
136    pub on_click: Option<Rc<dyn Fn()>>,
137    pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
138    pub focusable: bool,
139    pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
140    pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
141    pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
142    pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
143    pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
144    pub z_index: f32,
145    pub on_text_change: Option<Rc<dyn Fn(String)>>,
146    pub on_text_submit: Option<Rc<dyn Fn(String)>>,
147    /// If this hit region belongs to a TextField, this persistent key is used
148    /// for looking up platform-managed TextFieldState. Falls back to `id` if None.
149    pub tf_state_key: Option<u64>,
150}
151
152/// Flattened semantics node produced by `layout_and_paint`.
153///
154/// This is the source of truth for accessibility backends: it contains the
155/// resolved screen rect, role, label, and focus/enabled state.
156///
157/// The platform runner should convert this into OS‑specific accessibility trees (when implemented)
158/// (AT‑SPI on Linux, TalkBack on Android, etc.).
159#[derive(Clone)]
160pub struct SemNode {
161    /// Stable id, shared with the associated `HitRegion` / `ViewId`.
162    pub id: u64,
163    pub role: Role,
164    pub label: Option<String>,
165    pub rect: Rect,
166    pub focused: bool,
167    pub enabled: bool,
168}
169
170pub struct Scheduler {
171    next_id: u64,
172    pub focused: Option<u64>,
173    pub size: (u32, u32),
174}
175
176impl Scheduler {
177    pub fn new() -> Self {
178        Self {
179            next_id: 1,
180            focused: None,
181            size: (1280, 800),
182        }
183    }
184
185    pub fn id(&mut self) -> u64 {
186        let id = self.next_id;
187        self.next_id += 1;
188        id
189    }
190
191    pub fn repose<F>(
192        &mut self,
193        mut build_root: F,
194        layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
195    ) -> Frame
196    where
197        F: FnMut(&mut Scheduler) -> View,
198    {
199        let guard = ComposeGuard::begin();
200        let root = guard.scope.run(|| build_root(self));
201        let (scene, hits, sem) = layout_paint(&root, self.size);
202
203        let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
204
205        Frame {
206            scene,
207            hit_regions: hits,
208            semantics_nodes: sem,
209            focus_chain,
210        }
211    }
212}