use std::cell::RefCell;
use std::rc::Rc;
use geometry_core::Rect;
use layout_core::{LayoutError, LayoutStyle, NodeId};
use platform_core::Event;
use reactive_core::{Effect, RwSignal, effect, signal};
use ui_tree::{Component, EventResult, RenderNode};
use crate::context::{mark_dirty, new_container, set_children, set_display, track_layout};
use crate::layout_item::{LayoutItem, TrackedChildren, make_child};
use crate::pointer::dispatch_container_event;
type LazyBuild = Box<dyn FnOnce() -> Result<Vec<Box<dyn LayoutItem>>, LayoutError>>;
struct LazyState {
node: NodeId,
children: TrackedChildren,
build: Option<LazyBuild>,
}
pub struct Lazy {
node: NodeId,
rect: RwSignal<Rect>,
state: Rc<RefCell<LazyState>>,
visible: Rc<dyn Fn() -> bool>,
version: RwSignal<u64>,
_effect: Effect,
}
impl Lazy {
pub fn new(
container_style: LayoutStyle,
visible: impl Fn() -> bool + 'static,
build: impl FnOnce() -> Result<Vec<Box<dyn LayoutItem>>, LayoutError> + 'static,
) -> Result<Self, LayoutError> {
let node = new_container(container_style, &[])?;
let rect = track_layout(node).expect("lazy container is registered");
let state = Rc::new(RefCell::new(LazyState {
node,
children: Vec::new(),
build: Some(Box::new(build)),
}));
let version = signal(0u64);
let visible: Rc<dyn Fn() -> bool> = Rc::new(visible);
let eff_state = Rc::clone(&state);
let eff_version = version.clone();
let eff_visible = Rc::clone(&visible);
let _effect = effect(move || {
let show = eff_visible();
if show && realize(&eff_state) {
eff_version.update(|v| *v = v.wrapping_add(1));
}
set_display(node, show);
mark_dirty(node).ok();
});
Ok(Self {
node,
rect,
state,
visible,
version,
_effect,
})
}
pub fn is_built(&self) -> bool {
self.state.borrow().build.is_none()
}
}
fn realize(state: &Rc<RefCell<LazyState>>) -> bool {
let Some(build) = state.borrow_mut().build.take() else {
return false;
};
let Ok(items) = build() else {
return false;
};
let children: TrackedChildren = items.into_iter().map(make_child).collect();
let nodes: Vec<NodeId> = children.iter().map(|c| c.node()).collect();
let mut st = state.borrow_mut();
st.children = children;
let container = st.node;
drop(st);
let _ = set_children(container, &nodes);
true
}
impl LayoutItem for Lazy {
fn layout_node(&self) -> NodeId {
self.node
}
}
impl Component for Lazy {
fn view(&self) -> RenderNode {
let show = (self.visible)();
self.version.get();
let _ = self.rect.get();
if !show {
return RenderNode::Empty;
}
let st = self.state.borrow();
RenderNode::group(st.children.iter().map(|c| c.segment.boundary()))
}
fn on_event(&mut self, event: &Event) -> EventResult {
if !(self.visible)() {
return EventResult::Ignored;
}
let mut st = self.state.borrow_mut();
dispatch_container_event(&mut st.children, event)
}
fn debug_name(&self) -> &'static str {
"Lazy"
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use layout_core::AvailableSpace;
use reactive_core::signal;
use super::*;
use crate::container::Container;
use crate::context::{compute_layout, reset_layout_runtime};
fn leaf() -> Result<Box<dyn LayoutItem>, LayoutError> {
Ok(Box::new(Container::new(
LayoutStyle::new().width(10.0).height(10.0),
vec![],
)?))
}
#[test]
fn defers_construction_until_the_condition_first_holds() {
reset_layout_runtime();
let show = signal(false);
let builds = Rc::new(Cell::new(0));
let lazy = {
let (cond, builds) = (show.clone(), builds.clone());
Lazy::new(
LayoutStyle::new().flex_column(),
move || cond.get(),
move || {
builds.set(builds.get() + 1);
Ok(vec![leaf()?])
},
)
.unwrap()
};
assert_eq!(builds.get(), 0, "a block never shown costs nothing");
assert!(!lazy.is_built());
assert!(matches!(lazy.view(), RenderNode::Empty));
show.set(true);
assert_eq!(builds.get(), 1, "the first showing builds the subtree");
assert!(lazy.is_built());
}
#[test]
fn builds_once_and_only_toggles_afterwards() {
reset_layout_runtime();
let show = signal(true);
let builds = Rc::new(Cell::new(0));
let lazy = {
let (cond, builds) = (show.clone(), builds.clone());
Lazy::new(
LayoutStyle::new().flex_column(),
move || cond.get(),
move || {
builds.set(builds.get() + 1);
Ok(vec![leaf()?])
},
)
.unwrap()
};
assert_eq!(builds.get(), 1, "an initially-true block builds at once");
let node = lazy.state.borrow().children[0].node();
show.set(false);
show.set(true);
show.set(false);
show.set(true);
assert_eq!(builds.get(), 1, "reopening never rebuilds");
assert_eq!(
lazy.state.borrow().children[0].node(),
node,
"it is the same subtree, not a fresh one"
);
}
#[test]
fn a_hidden_block_takes_no_space() {
reset_layout_runtime();
let show = signal(true);
let lazy = {
let cond = show.clone();
Lazy::new(
LayoutStyle::new().flex_column(),
move || cond.get(),
|| Ok(vec![leaf()?]),
)
.unwrap()
};
let node = lazy.layout_node();
compute_layout(
node,
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
assert!(track_layout(node).unwrap().get().height > 0.0);
show.set(false);
compute_layout(
node,
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
assert_eq!(track_layout(node).unwrap().get().height, 0.0);
}
#[test]
fn a_hidden_block_ignores_events() {
reset_layout_runtime();
let show = signal(false);
let mut lazy = {
let cond = show.clone();
Lazy::new(
LayoutStyle::new().flex_column(),
move || cond.get(),
|| Ok(vec![leaf()?]),
)
.unwrap()
};
assert_eq!(
lazy.on_event(&Event::CursorEntered),
EventResult::Ignored,
"an unbuilt block answers for nothing"
);
}
}