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