use std::cell::{Cell, Ref, RefCell};
use std::rc::Rc;
use geometry_core::Rect;
use reactive_core::{Effect, RwSignal, effect, signal};
use renderer_core::DrawCommand;
use crate::component::Component;
use crate::render_node::RenderNode;
reactive_core::surface_local! {
slot FORCE_TICK: RwSignal<u64> = signal(0);
access with_force_tick, with_force_tick_ref;
context ForceTickContext, ForceTickGuard;
}
fn force_tick() -> RwSignal<u64> {
with_force_tick_ref(|s| s.clone())
}
pub fn bump_force_ticks() {
let tick = force_tick();
tick.set(tick.peek().wrapping_add(1));
}
type ChildSlots = Vec<(usize, Rc<Segment>, bool)>;
#[allow(clippy::large_enum_variant)]
enum Step {
Node(RenderNode),
EndOverlay,
}
pub struct Segment {
name: &'static str,
own_commands: Rc<RefCell<Vec<(DrawCommand, bool)>>>,
child_slots: Rc<RefCell<ChildSlots>>,
is_dirty: Rc<Cell<bool>>,
_effect: Effect,
}
#[derive(Clone, Debug)]
pub struct SegmentNodeInfo {
pub id: u64,
pub name: &'static str,
pub depth: usize,
pub rect: Rect,
}
fn union_nonempty(a: Rect, b: Rect) -> Rect {
let a_empty = a.width <= 0.0 || a.height <= 0.0;
let b_empty = b.width <= 0.0 || b.height <= 0.0;
match (a_empty, b_empty) {
(true, _) => b,
(_, true) => a,
_ => a.union(b),
}
}
impl Segment {
pub fn mount<C: Component + 'static>(component: C) -> Rc<Segment> {
Self::mount_dyn(Rc::new(RefCell::new(component)))
}
pub fn mount_dyn(component: Rc<RefCell<dyn Component>>) -> Rc<Segment> {
let name = component
.try_borrow()
.map(|c| c.debug_name())
.unwrap_or("Component");
Self::mount_fn_named(name, move || component.try_borrow().ok().map(|c| c.view()))
}
pub fn mount_fn_named(
name: &'static str,
render: impl Fn() -> Option<RenderNode> + 'static,
) -> Rc<Segment> {
let own_commands: Rc<RefCell<Vec<(DrawCommand, bool)>>> = Default::default();
let child_slots: Rc<RefCell<ChildSlots>> = Default::default();
let stack: Rc<RefCell<Vec<Step>>> = Default::default();
let is_dirty = Rc::new(Cell::new(true));
let own_c = Rc::clone(&own_commands);
let slots_c = Rc::clone(&child_slots);
let dirty_c = Rc::clone(&is_dirty);
let _effect = effect(move || {
force_tick().get(); let Some(node) = render() else {
return; };
let mut own = own_c.borrow_mut();
let mut stk = stack.borrow_mut();
let mut new_slots: ChildSlots = Vec::new();
let own_changed = flatten_segment(node, &mut own, &mut new_slots, &mut stk);
drop(stk);
drop(own);
let mut slots = slots_c.borrow_mut();
let slots_changed = slots.len() != new_slots.len()
|| slots
.iter()
.zip(new_slots.iter())
.any(|(a, b)| a.0 != b.0 || a.2 != b.2 || !Rc::ptr_eq(&a.1, &b.1));
if own_changed || slots_changed {
*slots = new_slots;
dirty_c.set(true);
}
});
Rc::new(Segment {
name,
own_commands,
child_slots,
is_dirty,
_effect,
})
}
pub fn boundary(self: &Rc<Self>) -> RenderNode {
RenderNode::Boundary {
child: Rc::clone(self),
}
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn walk(&self, out: &mut Vec<SegmentNodeInfo>) {
self.collect(0, out);
}
fn collect(&self, depth: usize, out: &mut Vec<SegmentNodeInfo>) -> Rect {
let idx = out.len();
out.push(SegmentNodeInfo {
id: idx as u64,
name: self.name,
depth,
rect: Rect::default(),
});
let mut bounds = Rect::default();
for (cmd, _) in self.own_commands.borrow().iter() {
let Some(rect) = renderer_core::culling::command_visual_rect(
cmd,
geometry_core::Transform::IDENTITY.to_array(),
&renderer_core::culling::FontMetrics::default(),
) else {
continue;
};
bounds = union_nonempty(bounds, rect);
}
for (_, child, _) in self.child_slots.borrow().iter() {
bounds = union_nonempty(bounds, child.collect(depth + 1, out));
}
out[idx].rect = bounds;
bounds
}
}
fn flatten_segment(
root: RenderNode,
out: &mut Vec<(DrawCommand, bool)>,
slots: &mut ChildSlots,
stack: &mut Vec<Step>,
) -> bool {
stack.clear();
stack.push(Step::Node(root));
let mut pos: usize = 0;
let mut changed = false;
let mut overlay_depth: usize = 0;
macro_rules! emit_command {
($command:expr) => {{
let entry = ($command, overlay_depth > 0);
if pos < out.len() {
if out[pos] != entry {
out[pos] = entry;
changed = true;
}
} else {
out.push(entry);
changed = true;
}
pos += 1;
}};
}
while let Some(step) = stack.pop() {
let node = match step {
Step::EndOverlay => {
overlay_depth -= 1;
continue;
}
Step::Node(node) => node,
};
match node {
RenderNode::Empty => {}
RenderNode::Primitive(cmd) => emit_command!(cmd),
RenderNode::Group { children } => {
for child in children.into_iter().rev() {
stack.push(Step::Node(child));
}
}
RenderNode::Transform { matrix, children } => {
stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopMatrix)));
for child in children.into_iter().rev() {
stack.push(Step::Node(child));
}
emit_command!(DrawCommand::PushMatrix { matrix });
}
RenderNode::Clip {
rect,
radius,
children,
} => {
stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopClip)));
for child in children.into_iter().rev() {
stack.push(Step::Node(child));
}
emit_command!(DrawCommand::PushClip { rect, radius });
}
RenderNode::Layer {
opacity,
backdrop_blur,
children,
} => {
stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopLayer)));
for child in children.into_iter().rev() {
stack.push(Step::Node(child));
}
emit_command!(DrawCommand::PushLayer {
opacity,
backdrop_blur
});
}
RenderNode::Overlay { children } => {
overlay_depth += 1;
stack.push(Step::EndOverlay);
for child in children.into_iter().rev() {
stack.push(Step::Node(child));
}
}
RenderNode::Boundary { child } => slots.push((pos, child, overlay_depth > 0)),
}
}
if pos != out.len() {
out.truncate(pos);
changed = true;
}
changed
}
pub(crate) fn compose_into(
seg: &Segment,
out: &mut Vec<DrawCommand>,
overlay_out: &mut Vec<DrawCommand>,
in_overlay: bool,
) {
seg.is_dirty.set(false);
let own_commands = seg.own_commands.borrow();
let slots = seg.child_slots.borrow();
let mut si = 0;
for (i, (cmd, is_overlay)) in own_commands.iter().enumerate() {
while si < slots.len() && slots[si].0 == i {
compose_into(&slots[si].1, out, overlay_out, in_overlay || slots[si].2);
si += 1;
}
if in_overlay || *is_overlay {
overlay_out.push(cmd.clone());
} else {
out.push(cmd.clone());
}
}
while si < slots.len() {
compose_into(&slots[si].1, out, overlay_out, in_overlay || slots[si].2);
si += 1;
}
}
fn any_dirty(seg: &Segment) -> bool {
if seg.is_dirty.get() {
return true;
}
seg.child_slots
.borrow()
.iter()
.any(|(_, child, _)| any_dirty(child))
}
pub struct SegmentRoot {
root: Rc<Segment>,
cached: RefCell<Vec<DrawCommand>>,
compose_generation: Cell<u64>,
cache_valid: Cell<bool>,
}
impl SegmentRoot {
pub fn mount<C: Component + 'static>(component: C) -> Self {
Self::from_segment(Segment::mount(component))
}
pub fn from_segment(root: Rc<Segment>) -> Self {
SegmentRoot {
root,
cached: RefCell::new(Vec::new()),
compose_generation: Cell::new(0),
cache_valid: Cell::new(false),
}
}
pub fn generation(&self) -> u64 {
self.compose_generation.get()
}
pub fn walk(&self, out: &mut Vec<SegmentNodeInfo>) {
self.root.walk(out);
}
pub fn is_dirty(&self) -> bool {
!self.cache_valid.get() || any_dirty(&self.root)
}
pub fn commands(&self) -> Ref<'_, Vec<DrawCommand>> {
if !self.cache_valid.get() || any_dirty(&self.root) {
let mut cached = self.cached.borrow_mut();
cached.clear();
let mut overlay: Vec<DrawCommand> = Vec::new();
compose_into(&self.root, &mut cached, &mut overlay, false); cached.extend(overlay);
drop(cached);
self.compose_generation
.set(self.compose_generation.get().wrapping_add(1));
self.cache_valid.set(true);
}
self.cached.borrow()
}
}
#[cfg(test)]
mod tests {
use geometry_core::Rect;
use reactive_core::{RwSignal, signal};
use renderer_core::{Color, RectStyle, ShapeStyle};
use super::*;
fn rect(x: f32) -> RenderNode {
RenderNode::rect(
Rect::new(x, 0.0, 10.0, 10.0),
RectStyle::default().with_fill(Color::BLACK),
)
}
struct Leaf {
x: RwSignal<f32>,
}
impl Component for Leaf {
fn view(&self) -> RenderNode {
RenderNode::group([rect(self.x.get()), rect(self.x.get() + 5.0)])
}
}
struct Parent {
children: Vec<Rc<Segment>>,
}
impl Component for Parent {
fn view(&self) -> RenderNode {
RenderNode::group(self.children.iter().map(|s| s.boundary()))
}
}
struct Nested;
impl Component for Nested {
fn view(&self) -> RenderNode {
RenderNode::group([
rect(0.0),
RenderNode::group([rect(1.0), RenderNode::Empty, RenderNode::group([rect(2.0)])]),
rect(3.0),
])
}
}
#[test]
fn flatten_nested_groups_and_empties() {
let root = SegmentRoot::mount(Nested);
assert_eq!(root.commands().len(), 4);
}
#[test]
fn composes_children_in_order() {
let a = signal(0.0f32);
let b = signal(100.0f32);
let (sa, sb) = (a.clone(), b.clone());
let children = vec![
Segment::mount(Leaf { x: sa }),
Segment::mount(Leaf { x: sb }),
];
let root = SegmentRoot::mount(Parent { children });
assert_eq!(root.commands().len(), 4);
}
fn cmd_x(c: &DrawCommand) -> f32 {
match c {
DrawCommand::Rect { rect, .. } => rect.x,
_ => -1.0,
}
}
struct WithOverlay;
impl Component for WithOverlay {
fn view(&self) -> RenderNode {
RenderNode::group([rect(1.0), RenderNode::overlay([rect(2.0)]), rect(3.0)])
}
}
#[test]
fn overlay_hoists_to_end() {
let root = SegmentRoot::mount(WithOverlay);
let cmds = root.commands();
let xs: Vec<f32> = cmds.iter().map(cmd_x).collect();
assert_eq!(xs, vec![1.0, 3.0, 2.0]);
}
struct OverlayParent {
child: Rc<Segment>,
}
impl Component for OverlayParent {
fn view(&self) -> RenderNode {
RenderNode::group([rect(1.0), RenderNode::overlay([self.child.boundary()])])
}
}
#[test]
fn overlay_hoists_child_segment() {
let child = Segment::mount(Leaf { x: signal(9.0) }); let root = SegmentRoot::mount(OverlayParent { child });
let cmds = root.commands();
let xs: Vec<f32> = cmds.iter().map(cmd_x).collect();
assert_eq!(xs, vec![1.0, 9.0, 14.0]);
}
#[test]
fn child_change_updates_output_without_parent_rerun() {
let a = signal(0.0f32);
let sa = a.clone();
let children = vec![Segment::mount(Leaf { x: sa })];
let root = SegmentRoot::mount(Parent { children });
let g0 = root.generation();
let first_x = match &root.commands()[0] {
DrawCommand::Rect { rect, .. } => rect.x,
_ => unreachable!(),
};
assert_eq!(first_x, 0.0);
a.set(42.0);
assert_ne!(root.generation(), g0, "child change must bump generation");
let new_x = match &root.commands()[0] {
DrawCommand::Rect { rect, .. } => rect.x,
_ => unreachable!(),
};
assert_eq!(new_x, 42.0, "composed output reflects the child update");
}
struct MemoLeaf {
double: reactive_core::Memo<i32>,
}
impl Component for MemoLeaf {
fn view(&self) -> RenderNode {
rect(self.double.get() as f32)
}
}
#[test]
fn signal_dependent_segment_updates_with_runner_batching() {
use reactive_core::{begin_batch, end_batch};
let a = signal(0.0f32);
let sa = a.clone();
let root = SegmentRoot::mount(Leaf { x: sa });
assert_eq!(animated_rect_x(&root), 0.0);
begin_batch();
a.set(42.0);
end_batch();
begin_batch();
let mid = animated_rect_x(&root);
end_batch();
assert_eq!(
mid, 42.0,
"signal-reading segment must reflect the batched set"
);
}
#[test]
fn memo_dependent_segment_updates_with_runner_batching() {
use reactive_core::{begin_batch, end_batch, memo};
let count = signal(0i32);
let count_mv = count.clone();
let double = memo(move || count_mv.get() * 2);
let root = SegmentRoot::mount(MemoLeaf {
double: double.clone(),
});
assert_eq!(animated_rect_x(&root), 0.0);
begin_batch();
count.set(3);
end_batch();
begin_batch();
let mid = animated_rect_x(&root);
end_batch();
assert_eq!(
mid, 6.0,
"memo-reading segment must reflect the flushed memo"
);
}
struct ThemedButton {
theme: RwSignal<f32>,
sel: RwSignal<i32>,
}
impl Component for ThemedButton {
fn view(&self) -> RenderNode {
let c = self.theme.get(); self.sel.get(); RenderNode::rect(
Rect::new(0.0, 0.0, 10.0, 10.0),
RectStyle::default().with_fill(Color::rgba(c, c, c, 1.0)),
)
}
fn on_event(&mut self, _event: &platform_core::Event) -> crate::component::EventResult {
self.sel.update(|n| *n += 1); crate::component::EventResult::Handled
}
}
fn first_rect_r(root: &SegmentRoot) -> f32 {
match &root.commands()[0] {
DrawCommand::Rect { style, .. } => style.fill.unwrap().solid_color().r,
_ => unreachable!(),
}
}
#[test]
fn dispatch_must_be_batched_or_segment_drops_subscriptions() {
use reactive_core::{batch, signal};
{
let theme = signal(0.2f32);
let sel = signal(0i32);
let widget = Rc::new(RefCell::new(ThemedButton {
theme: theme.clone(),
sel: sel.clone(),
}));
let render = {
let w = Rc::clone(&widget);
move || w.try_borrow().ok().map(|c| c.view())
};
let root = SegmentRoot::from_segment(Segment::mount_fn_named("Component", render));
assert!((first_rect_r(&root) - 0.2).abs() < 1e-6);
widget
.borrow_mut()
.on_event(&platform_core::Event::CursorLeft); theme.set(0.9);
assert!(
(first_rect_r(&root) - 0.2).abs() < 1e-6,
"unbatched dispatch must drop the theme subscription (frozen at old value)"
);
}
{
let theme = signal(0.2f32);
let sel = signal(0i32);
let widget = Rc::new(RefCell::new(ThemedButton {
theme: theme.clone(),
sel: sel.clone(),
}));
let render = {
let w = Rc::clone(&widget);
move || w.try_borrow().ok().map(|c| c.view())
};
let root = SegmentRoot::from_segment(Segment::mount_fn_named("Component", render));
assert!((first_rect_r(&root) - 0.2).abs() < 1e-6);
batch(|| {
widget
.borrow_mut()
.on_event(&platform_core::Event::CursorLeft)
});
theme.set(0.9);
assert!(
(first_rect_r(&root) - 0.9).abs() < 1e-6,
"batched dispatch must preserve the theme subscription (tracks new value)"
);
}
}
struct AnimatedLeaf {
x: motion_core::Animated<f32>,
}
impl Component for AnimatedLeaf {
fn view(&self) -> RenderNode {
rect(self.x.get())
}
}
fn animated_rect_x(root: &SegmentRoot) -> f32 {
match &root.commands()[0] {
DrawCommand::Rect { rect, .. } => rect.x,
_ => unreachable!(),
}
}
#[test]
fn animated_get_reflects_tick_in_commands_and_settles() {
use std::time::{Duration, Instant};
motion_core::reset();
motion_core::set_scale(1.0);
let anim = motion_core::Animated::new(
0.0f32,
motion_core::tween(Duration::from_millis(100), motion_core::Easing::Linear),
);
let root = SegmentRoot::mount(AnimatedLeaf { x: anim.clone() });
assert_eq!(animated_rect_x(&root), 0.0);
let g0 = root.generation();
anim.retarget(10.0);
assert!(
motion_core::has_active(),
"retarget must register an active animation"
);
let base = Instant::now();
motion_core::tick(base);
assert_eq!(
root.generation(),
g0,
"the t0-establishing tick must not recompose"
);
assert_eq!(animated_rect_x(&root), 0.0);
motion_core::tick(base + Duration::from_millis(50));
let mid_x = animated_rect_x(&root);
let g1 = root.generation();
assert!(
(mid_x - 5.0).abs() < 1e-3,
"expected the midpoint of the tween, got {mid_x}"
);
assert_ne!(g1, g0, "an in-flight tick must bump the compose generation");
motion_core::tick(base + Duration::from_millis(100));
let end_x = animated_rect_x(&root);
let g2 = root.generation();
assert_eq!(end_x, 10.0);
assert_ne!(g2, g1, "the settling tick must still bump the generation");
assert!(
!motion_core::has_active(),
"a settled tween must deregister"
);
motion_core::tick(base + Duration::from_millis(200));
assert_eq!(animated_rect_x(&root), 10.0);
assert_eq!(
root.generation(),
g2,
"a tick with no active animations must not bump the generation"
);
}
}