use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::{Rc, Weak};
use layout_reactive::{LayoutContext, LayoutGuard};
use platform_core::{WindowCommandContext, WindowCommandGuard};
use reactive_core::{
SurfaceEnterGuard, SurfaceHandle, set_current_surface, set_surface_enter_hook,
};
use services_core::{ServiceContext, ServiceGuard};
use ui_tree::{ForceTickContext, ForceTickGuard, OverlayContext, OverlayGuard};
use crate::focus::{FocusContext, FocusGuard};
use crate::input_region::{InputRegionContext, InputRegionGuard};
pub struct Surface {
handle: SurfaceHandle,
layout: LayoutContext,
overlay: OverlayContext,
focus: FocusContext,
input_region: InputRegionContext,
force_tick: ForceTickContext,
window_commands: WindowCommandContext,
services: ServiceContext,
}
impl Surface {
pub fn new() -> Rc<Self> {
install_enter_hook();
let handle = next_handle();
let surface = Rc::new(Self {
handle,
layout: LayoutContext::new(),
overlay: OverlayContext::new(),
focus: FocusContext::new(),
input_region: InputRegionContext::new(),
force_tick: ForceTickContext::new(),
window_commands: WindowCommandContext::new(),
services: ServiceContext::new(),
});
SURFACES.with(|s| s.borrow_mut().insert(handle, Rc::downgrade(&surface)));
surface
}
pub fn handle(&self) -> SurfaceHandle {
self.handle
}
#[must_use = "the surface is only active while this guard is alive"]
pub fn enter(&self) -> SurfaceGuard {
let prev_surface = set_current_surface(self.handle);
SurfaceGuard {
_layout: self.layout.enter(),
_overlay: self.overlay.enter(),
_focus: self.focus.enter(),
_input_region: self.input_region.enter(),
_force_tick: self.force_tick.enter(),
_window_commands: self.window_commands.enter(),
_services: self.services.enter(),
_prev_surface: RestoreSurface(prev_surface),
}
}
}
impl Drop for Surface {
fn drop(&mut self) {
SURFACES.with(|s| {
s.borrow_mut().remove(&self.handle);
});
}
}
#[must_use = "the surface is only active while this guard is alive"]
pub struct SurfaceGuard {
_layout: LayoutGuard,
_overlay: OverlayGuard,
_focus: FocusGuard,
_input_region: InputRegionGuard,
_force_tick: ForceTickGuard,
_window_commands: WindowCommandGuard,
_services: ServiceGuard,
_prev_surface: RestoreSurface,
}
struct RestoreSurface(SurfaceHandle);
impl Drop for RestoreSurface {
fn drop(&mut self) {
set_current_surface(self.0);
}
}
thread_local! {
static SURFACES: RefCell<HashMap<SurfaceHandle, Weak<Surface>>> =
RefCell::new(HashMap::new());
static NEXT_HANDLE: Cell<u64> = const { Cell::new(1) };
static HOOK_INSTALLED: Cell<bool> = const { Cell::new(false) };
}
fn next_handle() -> SurfaceHandle {
NEXT_HANDLE.with(|c| {
let id = c.get();
c.set(id + 1);
SurfaceHandle(id)
})
}
fn install_enter_hook() {
HOOK_INSTALLED.with(|installed| {
if installed.replace(true) {
return;
}
set_surface_enter_hook(|handle| {
let surface = SURFACES.with(|s| s.borrow().get(&handle).and_then(Weak::upgrade));
match surface {
Some(surface) => {
let guard = surface.enter();
SurfaceEnterGuard::new(move || drop(guard))
}
None => SurfaceEnterGuard::noop(),
}
});
});
}
#[cfg(test)]
mod tests {
use reactive_core::{current_surface, effect, signal};
use super::Surface;
#[test]
fn effect_reenters_its_surface_layout_world() {
use layout_reactive::{
AvailableSpace, LayoutStyle, compute_layout, new_leaf, track_layout,
};
use std::cell::RefCell;
use std::rc::Rc;
let a = Surface::new();
let b = Surface::new();
assert_ne!(a.handle(), b.handle());
assert!(!a.handle().is_none());
let shared = signal(0i32);
let ran_under: Rc<RefCell<Vec<u64>>> = Rc::new(RefCell::new(Vec::new()));
let ran_c = Rc::clone(&ran_under);
let read = shared.read_only();
let a_node = {
let _g = a.enter();
let (node, _) = new_leaf(LayoutStyle::new().width(10.0).height(10.0)).unwrap();
let _e = effect(move || {
read.get();
ran_c.borrow_mut().push(current_surface().0);
});
std::mem::forget(_e);
node
};
ran_under.borrow_mut().clear();
{
let _g = b.enter();
shared.set(1);
}
assert_eq!(
ran_under.borrow().as_slice(),
&[a.handle().0],
"A's effect must run under A's surface, not B's"
);
{
let _g = a.enter();
compute_layout(
a_node,
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
assert_eq!(track_layout(a_node).unwrap().get().width, 10.0);
}
{
let _g = b.enter();
assert!(
track_layout(a_node).is_none(),
"A's node must not exist in B's layout world"
);
}
}
#[test]
fn provide_inject_is_per_surface_and_survives_into_effects() {
use std::cell::RefCell;
use std::rc::Rc;
use services_core::{provide, try_inject};
let a = Surface::new();
let b = Surface::new();
{
let _g = a.enter();
provide(String::from("A")).unwrap();
assert_eq!(try_inject::<String>().as_deref(), Some("A"));
}
{
let _g = b.enter();
provide(String::from("B")).unwrap();
assert_eq!(try_inject::<String>().as_deref(), Some("B"));
}
let shared = signal(0i32);
let read = shared.read_only();
let seen: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let seen_c = Rc::clone(&seen);
let ea = {
let _g = a.enter();
effect(move || {
read.get();
seen_c
.borrow_mut()
.push(try_inject::<String>().unwrap_or_default());
})
};
seen.borrow_mut().clear();
{
let _g = b.enter();
shared.set(1);
}
assert_eq!(
seen.borrow().as_slice(),
&[String::from("A")],
"A's effect must inject A's context even when fired from B"
);
drop(ea);
}
#[test]
fn global_signal_reruns_all_surfaces_each_under_its_context() {
use std::cell::RefCell;
use std::rc::Rc;
let a = Surface::new();
let b = Surface::new();
let global = signal(0i32);
let log: Rc<RefCell<Vec<(char, u64)>>> = Rc::new(RefCell::new(Vec::new()));
let log_a = Rc::clone(&log);
let read_a = global.read_only();
let ea = {
let _g = a.enter();
effect(move || {
read_a.get();
log_a.borrow_mut().push(('a', current_surface().0));
})
};
let log_b = Rc::clone(&log);
let read_b = global.read_only();
let eb = {
let _g = b.enter();
effect(move || {
read_b.get();
log_b.borrow_mut().push(('b', current_surface().0));
})
};
log.borrow_mut().clear();
global.set(1);
let entries = log.borrow().clone();
assert!(
entries.contains(&('a', a.handle().0)),
"A's effect must re-run under A: {entries:?}"
);
assert!(
entries.contains(&('b', b.handle().0)),
"B's effect must re-run under B: {entries:?}"
);
drop(ea);
drop(eb);
}
}