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, // spatial navigation not implemented at the core level
90        }
91    }
92
93    /// Tab forward or backward in the focus chain.
94    pub fn move_tab(&mut self, reverse: bool) -> Option<u64> {
95        if self.chain.is_empty() {
96            return None;
97        }
98        let next = if let Some(cur) = self.focused {
99            if let Some(idx) = self.chain.iter().position(|&id| id == cur) {
100                if reverse {
101                    if idx == 0 {
102                        self.chain[self.chain.len() - 1]
103                    } else {
104                        self.chain[idx - 1]
105                    }
106                } else {
107                    self.chain[(idx + 1) % self.chain.len()]
108                }
109            } else {
110                self.chain[0]
111            }
112        } else {
113            self.chain[0]
114        };
115        self.focused = Some(next);
116        Some(next)
117    }
118
119    /// Set target ID on a FocusRequester (called during layout).
120    pub fn set_requester_target(requester: &FocusRequester, id: u64) {
121        *requester.target.borrow_mut() = Some(id);
122    }
123}
124
125#[derive(Default)]
126pub struct Composer {
127    pub slots: Vec<Box<dyn Any>>,
128    pub cursor: usize,
129    pub keyed_slots: HashMap<String, Box<dyn Any>>,
130}
131
132pub struct ComposeGuard {
133    scope: Scope,
134}
135
136impl ComposeGuard {
137    pub fn begin() -> Self {
138        COMPOSER.with(|c| c.borrow_mut().cursor = 0);
139
140        let scope = ROOT_SCOPE.with(|rs| {
141            if let Some(existing) = rs.borrow().clone() {
142                existing
143            } else {
144                let s = Scope::new();
145                *rs.borrow_mut() = Some(s.clone());
146                s
147            }
148        });
149
150        ComposeGuard { scope }
151    }
152
153    pub fn scope(&self) -> &Scope {
154        &self.scope
155    }
156}
157
158impl Drop for ComposeGuard {
159    fn drop(&mut self) {
160        // ROOT_SCOPE.with(|rs| { Do not clear every frame
161        //     *rs.borrow_mut() = None;
162        // });
163    }
164}
165
166/// Slot-based remember (sequential composition only)
167pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Rc<T> {
168    COMPOSER.with(|c| {
169        let mut c = c.borrow_mut();
170        let cursor = c.cursor;
171        c.cursor += 1;
172
173        if cursor >= c.slots.len() {
174            let rc: Rc<T> = Rc::new(init());
175            c.slots.push(Box::new(rc.clone()));
176            return rc;
177        }
178
179        if let Some(rc) = c.slots[cursor].downcast_ref::<Rc<T>>() {
180            rc.clone()
181        } else {
182            // replace (else panics)
183            log::warn!(
184                "remember: slot {} type changed; replacing. \
185                 If this is due to conditional composition, prefer remember_with_key.",
186                cursor
187            );
188            let rc: Rc<T> = Rc::new(init());
189            c.slots[cursor] = Box::new(rc.clone());
190            rc
191        }
192    })
193}
194
195/// Key-based remember
196pub fn remember_with_key<T: 'static>(key: impl Into<String>, init: impl FnOnce() -> T) -> Rc<T> {
197    COMPOSER.with(|c| {
198        let mut c = c.borrow_mut();
199        let key = key.into();
200
201        if let Some(existing) = c.keyed_slots.get(&key) {
202            if let Some(rc) = existing.downcast_ref::<Rc<T>>() {
203                return rc.clone();
204            } else {
205                log::warn!(
206                    "remember_with_key: key '{}' reused with a different type; replacing.",
207                    key
208                );
209            }
210        }
211
212        if cfg!(debug_assertions) && c.keyed_slots.len() > 10_000 {
213            log::warn!(
214                "remember_with_key: more than 10k keys stored; \
215                are you generating unbounded dynamic keys (e.g., using timestamps)?"
216            );
217        }
218
219        let rc: Rc<T> = Rc::new(init());
220        c.keyed_slots.insert(key, Box::new(rc.clone()));
221        rc
222    })
223}
224
225pub fn remember_state<T: 'static>(init: impl FnOnce() -> T) -> Rc<RefCell<T>> {
226    remember(|| RefCell::new(init()))
227}
228
229pub fn remember_state_with_key<T: 'static>(
230    key: impl Into<String>,
231    init: impl FnOnce() -> T,
232) -> Rc<RefCell<T>> {
233    remember_with_key(key, || RefCell::new(init()))
234}
235
236/// Frame - output of composition for a tick: scene + input/semantics.
237pub struct Frame {
238    pub scene: Scene,
239    pub hit_regions: Vec<HitRegion>,
240    pub semantics_nodes: Vec<SemNode>,
241    pub focus_chain: Vec<u64>,
242}
243
244#[derive(Clone, Default)]
245pub struct HitRegion {
246    pub id: u64,
247    pub rect: Rect,
248    pub on_click: Option<Rc<dyn Fn()>>,
249    pub on_scroll: Option<Rc<dyn Fn(crate::Vec2) -> crate::Vec2>>,
250    pub focusable: bool,
251    pub on_pointer_down: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
252    pub on_pointer_move: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
253    pub on_pointer_up: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
254    pub on_pointer_enter: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
255    pub on_pointer_leave: Option<Rc<dyn Fn(crate::input::PointerEvent)>>,
256    pub z_index: f32,
257    pub on_text_change: Option<Rc<dyn Fn(String)>>,
258    pub on_text_submit: Option<Rc<dyn Fn(String)>>,
259    /// If this hit region belongs to a TextField, this persistent key is used
260    /// for looking up platform-managed TextFieldState. Falls back to `id` if None.
261    pub tf_state_key: Option<u64>,
262
263    /// True if this hit region corresponds to a multiline text input (TextArea).
264    pub tf_multiline: bool,
265
266    // internal
267    pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
268    pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
269    pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
270    pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
271    pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
272    pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
273
274    pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
275
276    /// Cursor hint for desktop/web.
277    pub cursor: Option<crate::CursorIcon>,
278}
279
280impl HitRegion {
281    /// Seed a HitRegion with all the modifier's event handlers + dnd + cursor.
282    /// Call‑sites should only override the fields that differ (on_click, focusable, etc.)
283    /// via struct‑update syntax: `HitRegion { focusable: true, ..from_modifier(..) }`.
284    pub fn from_modifier(id: u64, rect: Rect, m: &crate::modifier::Modifier) -> Self {
285        Self {
286            id,
287            rect,
288            z_index: m.z_index,
289            on_pointer_down: m.on_pointer_down.clone(),
290            on_pointer_move: m.on_pointer_move.clone(),
291            on_pointer_up: m.on_pointer_up.clone(),
292            on_pointer_enter: m.on_pointer_enter.clone(),
293            on_pointer_leave: m.on_pointer_leave.clone(),
294            on_action: m.on_action.clone(),
295            cursor: m.cursor,
296            on_drag_start: m.on_drag_start.clone(),
297            on_drag_end: m.on_drag_end.clone(),
298            on_drag_enter: m.on_drag_enter.clone(),
299            on_drag_over: m.on_drag_over.clone(),
300            on_drag_leave: m.on_drag_leave.clone(),
301            on_drop: m.on_drop.clone(),
302            ..Default::default()
303        }
304    }
305}
306
307/// Flattened semantics node produced by `layout_and_paint`.
308///
309/// This is the source of truth for accessibility backends: it contains the
310/// resolved screen rect, role, label, and focus/enabled state.
311///
312/// The platform runner should convert this into OS‑specific accessibility trees (when implemented)
313/// (AT‑SPI on Linux, TalkBack on Android, etc.).
314#[derive(Clone)]
315pub struct SemNode {
316    /// Stable id, shared with the associated `HitRegion` / `ViewId`.
317    pub id: u64,
318
319    /// `None` means direct child of the window root.
320    pub parent: Option<u64>,
321
322    pub role: Role,
323    pub label: Option<String>,
324    pub rect: Rect,
325    pub focused: bool,
326    pub enabled: bool,
327}
328
329pub struct Scheduler {
330    next_id: u64,
331    pub focused: Option<u64>,
332    pub size: (u32, u32),
333}
334
335impl Default for Scheduler {
336    fn default() -> Self {
337        Self::new()
338    }
339}
340
341impl Scheduler {
342    pub fn new() -> Self {
343        Self {
344            next_id: 1,
345            focused: None,
346            size: (1280, 800),
347        }
348    }
349
350    pub fn id(&mut self) -> u64 {
351        let id = self.next_id;
352        self.next_id += 1;
353        id
354    }
355
356    pub fn id_count(&self) -> u64 {
357        self.next_id - 1
358    }
359
360    pub fn repose<F>(
361        &mut self,
362        mut build_root: F,
363        layout_paint: impl Fn(&View, (u32, u32)) -> (Scene, Vec<HitRegion>, Vec<SemNode>),
364    ) -> Frame
365    where
366        F: FnMut(&mut Scheduler) -> View,
367    {
368        let guard = ComposeGuard::begin();
369        let root = guard.scope.run(|| build_root(self));
370        let (scene, hits, sem) = layout_paint(&root, self.size);
371
372        let focus_chain: Vec<u64> = hits.iter().filter(|h| h.focusable).map(|h| h.id).collect();
373
374        Frame {
375            scene,
376            hit_regions: hits,
377            semantics_nodes: sem,
378            focus_chain,
379        }
380    }
381}
382
383/// Avoids cross-test pollution
384#[cfg(test)]
385pub fn clear_composer() {
386    COMPOSER.with(|c| {
387        let mut c = c.borrow_mut();
388        c.slots.clear();
389        c.keyed_slots.clear();
390        c.cursor = 0;
391    });
392    ROOT_SCOPE.with(|rs| {
393        *rs.borrow_mut() = None;
394    });
395}