Skip to main content

telar_reactive_core/runtime/
surface.rs

1use std::cell::{Cell, RefCell};
2use std::rc::Rc;
3
4/// Identifies which surface (window / layer-surface) a reactive effect belongs to. It is a cheap `Copy`
5/// id: the reactive runtime stamps one onto every effect at registration and re-enters that surface's
6/// context before running the effect during a flush, so an effect owned by surface A resolves its
7/// layout/overlay/focus world against A even when a signal set from surface B triggers it (the "owner
8/// scope" model of Solid/Floem).
9///
10/// [`SurfaceHandle::NONE`] is the ambient handle used when no surface context is active — single-window
11/// apps, or reactive work outside any surface. Entering `NONE`, or entering the already-active surface, is
12/// a no-op, so single-surface apps pay ~zero overhead.
13///
14/// reactive-core is the lowest crate and cannot know the per-surface thread-locals (layout/overlay/focus
15/// live in higher crates), so it only stores the id and calls an installed hook ([`set_surface_enter_hook`])
16/// to do the actual context switch.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct SurfaceHandle(pub u64);
19
20impl SurfaceHandle {
21    pub const NONE: SurfaceHandle = SurfaceHandle(0);
22
23    pub fn is_none(self) -> bool {
24        self.0 == 0
25    }
26
27    /// Activates this surface's context for as long as the returned guard lives. Fast-path no-op when this
28    /// surface is already active (single-window: entered once at the top, every effect re-enters the same
29    /// surface → guard is inert) or when no hook is installed.
30    pub fn enter(self) -> SurfaceEnterGuard {
31        if self == current_surface() {
32            return SurfaceEnterGuard::noop();
33        }
34        // Clone the hook Rc out of the borrow before calling it: the hook runs arbitrary context-swap code
35        // (and may itself enter surfaces), so it must not run while this thread-local is borrowed.
36        let hook = ENTER_HOOK.with(|h| h.borrow().clone());
37        match hook {
38            Some(f) => f(self),
39            None => SurfaceEnterGuard::noop(),
40        }
41    }
42}
43
44impl Default for SurfaceHandle {
45    fn default() -> Self {
46        Self::NONE
47    }
48}
49
50thread_local! {
51    static CURRENT_SURFACE: Cell<SurfaceHandle> = const { Cell::new(SurfaceHandle::NONE) };
52    static ENTER_HOOK: RefCell<Option<Rc<dyn Fn(SurfaceHandle) -> SurfaceEnterGuard>>> =
53        const { RefCell::new(None) };
54}
55
56/// The surface an effect registered right now would be owned by.
57pub fn current_surface() -> SurfaceHandle {
58    CURRENT_SURFACE.with(|c| c.get())
59}
60
61/// Sets the active surface, returning the previous one (so the caller can restore it). The surface layer's
62/// enter hook uses this; app code should go through `Surface::enter`.
63pub fn set_current_surface(handle: SurfaceHandle) -> SurfaceHandle {
64    CURRENT_SURFACE.with(|c| c.replace(handle))
65}
66
67/// Installs the thread's surface-context hook. Given a [`SurfaceHandle`], it must activate that surface's
68/// full per-surface world (the reactive current-surface plus the layout/overlay/focus/... thread-locals)
69/// and return a [`SurfaceEnterGuard`] that restores the previous world on drop. The higher-level `Surface`
70/// layer installs this; reactive-core only knows how to call it. Without a hook, [`SurfaceHandle::enter`]
71/// is a no-op — which is exactly right for single-window apps that never install one.
72pub fn set_surface_enter_hook(f: impl Fn(SurfaceHandle) -> SurfaceEnterGuard + 'static) {
73    ENTER_HOOK.with(|h| *h.borrow_mut() = Some(Rc::new(f)));
74}
75
76/// RAII guard restoring the surface context that was active before [`SurfaceHandle::enter`]. Produced by
77/// the installed hook (carrying a restore closure) or as an inert no-op.
78#[must_use = "the surface context is only active while this guard is alive"]
79pub struct SurfaceEnterGuard {
80    restore: Option<Box<dyn FnOnce()>>,
81}
82
83impl SurfaceEnterGuard {
84    pub fn noop() -> Self {
85        Self { restore: None }
86    }
87
88    pub fn new(restore: impl FnOnce() + 'static) -> Self {
89        Self {
90            restore: Some(Box::new(restore)),
91        }
92    }
93}
94
95impl Drop for SurfaceEnterGuard {
96    fn drop(&mut self) {
97        if let Some(f) = self.restore.take() {
98            f();
99        }
100    }
101}