1use std::cell::{Cell, Ref, RefCell};
11use std::rc::Rc;
12
13use geometry_core::Rect;
14use reactive_core::{Effect, RwSignal, effect, signal};
15use renderer_core::DrawCommand;
16
17use crate::component::Component;
18use crate::render_node::RenderNode;
19
20reactive_core::surface_local! {
21 slot FORCE_TICK: RwSignal<u64> = signal(0);
28 access with_force_tick, with_force_tick_ref;
29 context ForceTickContext, ForceTickGuard;
30}
31
32fn force_tick() -> RwSignal<u64> {
35 with_force_tick_ref(|s| s.clone())
36}
37
38pub fn bump_force_ticks() {
40 let tick = force_tick();
41 tick.set(tick.peek().wrapping_add(1));
42}
43
44type ChildSlots = Vec<(usize, Rc<Segment>, bool)>;
46
47#[allow(clippy::large_enum_variant)]
52enum Step {
53 Node(RenderNode),
54 EndOverlay,
55}
56
57pub struct Segment {
58 name: &'static str,
60 own_commands: Rc<RefCell<Vec<(DrawCommand, bool)>>>,
65 child_slots: Rc<RefCell<ChildSlots>>,
67 is_dirty: Rc<Cell<bool>>,
69 _effect: Effect,
70}
71
72#[derive(Clone, Debug)]
75pub struct SegmentNodeInfo {
76 pub id: u64,
77 pub name: &'static str,
78 pub depth: usize,
79 pub rect: Rect,
80}
81
82fn union_nonempty(a: Rect, b: Rect) -> Rect {
85 let a_empty = a.width <= 0.0 || a.height <= 0.0;
86 let b_empty = b.width <= 0.0 || b.height <= 0.0;
87 match (a_empty, b_empty) {
88 (true, _) => b,
89 (_, true) => a,
90 _ => a.union(b),
91 }
92}
93
94impl Segment {
95 pub fn mount<C: Component + 'static>(component: C) -> Rc<Segment> {
99 Self::mount_dyn(Rc::new(RefCell::new(component)))
100 }
101
102 pub fn mount_dyn(component: Rc<RefCell<dyn Component>>) -> Rc<Segment> {
107 let name = component
108 .try_borrow()
109 .map(|c| c.debug_name())
110 .unwrap_or("Component");
111 Self::mount_fn_named(name, move || component.try_borrow().ok().map(|c| c.view()))
112 }
113
114 pub fn mount_fn_named(
119 name: &'static str,
120 render: impl Fn() -> Option<RenderNode> + 'static,
121 ) -> Rc<Segment> {
122 let own_commands: Rc<RefCell<Vec<(DrawCommand, bool)>>> = Default::default();
123 let child_slots: Rc<RefCell<ChildSlots>> = Default::default();
124 let stack: Rc<RefCell<Vec<Step>>> = Default::default();
125 let is_dirty = Rc::new(Cell::new(true));
127
128 let own_c = Rc::clone(&own_commands);
129 let slots_c = Rc::clone(&child_slots);
130 let dirty_c = Rc::clone(&is_dirty);
131 let _effect = effect(move || {
132 force_tick().get(); let Some(node) = render() else {
134 return; };
136 let mut own = own_c.borrow_mut();
137 let mut stk = stack.borrow_mut();
138 let mut new_slots: ChildSlots = Vec::new();
139 let own_changed = flatten_segment(node, &mut own, &mut new_slots, &mut stk);
140 drop(stk);
141 drop(own);
142 let mut slots = slots_c.borrow_mut();
143 let slots_changed = slots.len() != new_slots.len()
145 || slots
146 .iter()
147 .zip(new_slots.iter())
148 .any(|(a, b)| a.0 != b.0 || a.2 != b.2 || !Rc::ptr_eq(&a.1, &b.1));
149 if own_changed || slots_changed {
150 *slots = new_slots;
151 dirty_c.set(true);
152 }
153 });
154
155 Rc::new(Segment {
156 name,
157 own_commands,
158 child_slots,
159 is_dirty,
160 _effect,
161 })
162 }
163
164 pub fn boundary(self: &Rc<Self>) -> RenderNode {
166 RenderNode::Boundary {
167 child: Rc::clone(self),
168 }
169 }
170
171 pub fn name(&self) -> &'static str {
173 self.name
174 }
175
176 pub fn walk(&self, out: &mut Vec<SegmentNodeInfo>) {
179 self.collect(0, out);
180 }
181
182 fn collect(&self, depth: usize, out: &mut Vec<SegmentNodeInfo>) -> Rect {
187 let idx = out.len();
188 out.push(SegmentNodeInfo {
190 id: idx as u64,
191 name: self.name,
192 depth,
193 rect: Rect::default(),
194 });
195
196 let mut bounds = Rect::default();
197 for (cmd, _) in self.own_commands.borrow().iter() {
198 let Some(rect) = renderer_core::culling::command_visual_rect(
202 cmd,
203 geometry_core::Transform::IDENTITY.to_array(),
204 &renderer_core::culling::FontMetrics::default(),
205 ) else {
206 continue;
207 };
208 bounds = union_nonempty(bounds, rect);
209 }
210
211 for (_, child, _) in self.child_slots.borrow().iter() {
212 bounds = union_nonempty(bounds, child.collect(depth + 1, out));
213 }
214
215 out[idx].rect = bounds;
216 bounds
217 }
218}
219
220fn flatten_segment(
224 root: RenderNode,
225 out: &mut Vec<(DrawCommand, bool)>,
226 slots: &mut ChildSlots,
227 stack: &mut Vec<Step>,
228) -> bool {
229 stack.clear();
230 stack.push(Step::Node(root));
231 let mut pos: usize = 0;
232 let mut changed = false;
233 let mut overlay_depth: usize = 0;
235
236 macro_rules! emit_command {
237 ($command:expr) => {{
238 let entry = ($command, overlay_depth > 0);
241 if pos < out.len() {
242 if out[pos] != entry {
243 out[pos] = entry;
244 changed = true;
245 }
246 } else {
247 out.push(entry);
248 changed = true;
249 }
250 pos += 1;
251 }};
252 }
253
254 while let Some(step) = stack.pop() {
255 let node = match step {
256 Step::EndOverlay => {
257 overlay_depth -= 1;
258 continue;
259 }
260 Step::Node(node) => node,
261 };
262 match node {
263 RenderNode::Empty => {}
264 RenderNode::Primitive(cmd) => emit_command!(cmd),
265 RenderNode::Group { children } => {
266 for child in children.into_iter().rev() {
267 stack.push(Step::Node(child));
268 }
269 }
270 RenderNode::Transform { matrix, children } => {
271 stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopMatrix)));
272 for child in children.into_iter().rev() {
273 stack.push(Step::Node(child));
274 }
275 emit_command!(DrawCommand::PushMatrix { matrix });
276 }
277 RenderNode::Clip {
278 rect,
279 radius,
280 children,
281 } => {
282 stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopClip)));
283 for child in children.into_iter().rev() {
284 stack.push(Step::Node(child));
285 }
286 emit_command!(DrawCommand::PushClip { rect, radius });
287 }
288 RenderNode::Layer {
289 opacity,
290 backdrop_blur,
291 children,
292 } => {
293 stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopLayer)));
294 for child in children.into_iter().rev() {
295 stack.push(Step::Node(child));
296 }
297 emit_command!(DrawCommand::PushLayer {
298 opacity,
299 backdrop_blur
300 });
301 }
302 RenderNode::Overlay { children } => {
304 overlay_depth += 1;
305 stack.push(Step::EndOverlay);
306 for child in children.into_iter().rev() {
307 stack.push(Step::Node(child));
308 }
309 }
310 RenderNode::Boundary { child } => slots.push((pos, child, overlay_depth > 0)),
313 }
314 }
315
316 if pos != out.len() {
317 out.truncate(pos);
318 changed = true;
319 }
320 changed
321}
322
323pub(crate) fn compose_into(
331 seg: &Segment,
332 out: &mut Vec<DrawCommand>,
333 overlay_out: &mut Vec<DrawCommand>,
334 in_overlay: bool,
335) {
336 seg.is_dirty.set(false);
337 let own_commands = seg.own_commands.borrow();
338 let slots = seg.child_slots.borrow();
339 let mut si = 0;
340 for (i, (cmd, is_overlay)) in own_commands.iter().enumerate() {
341 while si < slots.len() && slots[si].0 == i {
342 compose_into(&slots[si].1, out, overlay_out, in_overlay || slots[si].2);
343 si += 1;
344 }
345 if in_overlay || *is_overlay {
346 overlay_out.push(cmd.clone());
347 } else {
348 out.push(cmd.clone());
349 }
350 }
351 while si < slots.len() {
352 compose_into(&slots[si].1, out, overlay_out, in_overlay || slots[si].2);
353 si += 1;
354 }
355}
356
357fn any_dirty(seg: &Segment) -> bool {
360 if seg.is_dirty.get() {
361 return true;
362 }
363 seg.child_slots
364 .borrow()
365 .iter()
366 .any(|(_, child, _)| any_dirty(child))
367}
368
369pub struct SegmentRoot {
373 root: Rc<Segment>,
374 cached: RefCell<Vec<DrawCommand>>,
375 compose_generation: Cell<u64>,
377 cache_valid: Cell<bool>,
378}
379
380impl SegmentRoot {
381 pub fn mount<C: Component + 'static>(component: C) -> Self {
382 Self::from_segment(Segment::mount(component))
383 }
384
385 pub fn from_segment(root: Rc<Segment>) -> Self {
386 SegmentRoot {
387 root,
388 cached: RefCell::new(Vec::new()),
389 compose_generation: Cell::new(0),
390 cache_valid: Cell::new(false),
391 }
392 }
393
394 pub fn generation(&self) -> u64 {
395 self.compose_generation.get()
396 }
397
398 pub fn walk(&self, out: &mut Vec<SegmentNodeInfo>) {
400 self.root.walk(out);
401 }
402
403 pub fn is_dirty(&self) -> bool {
405 !self.cache_valid.get() || any_dirty(&self.root)
406 }
407
408 pub fn commands(&self) -> Ref<'_, Vec<DrawCommand>> {
409 if !self.cache_valid.get() || any_dirty(&self.root) {
410 let mut cached = self.cached.borrow_mut();
411 cached.clear();
412 let mut overlay: Vec<DrawCommand> = Vec::new();
415 compose_into(&self.root, &mut cached, &mut overlay, false); cached.extend(overlay);
417 drop(cached);
418 self.compose_generation
419 .set(self.compose_generation.get().wrapping_add(1));
420 self.cache_valid.set(true);
421 }
422 self.cached.borrow()
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use geometry_core::Rect;
429 use reactive_core::{RwSignal, signal};
430 use renderer_core::{Color, RectStyle, ShapeStyle};
431
432 use super::*;
433
434 fn rect(x: f32) -> RenderNode {
435 RenderNode::rect(
436 Rect::new(x, 0.0, 10.0, 10.0),
437 RectStyle::default().with_fill(Color::BLACK),
438 )
439 }
440
441 struct Leaf {
442 x: RwSignal<f32>,
443 }
444 impl Component for Leaf {
445 fn view(&self) -> RenderNode {
446 RenderNode::group([rect(self.x.get()), rect(self.x.get() + 5.0)])
447 }
448 }
449
450 struct Parent {
451 children: Vec<Rc<Segment>>,
452 }
453 impl Component for Parent {
454 fn view(&self) -> RenderNode {
455 RenderNode::group(self.children.iter().map(|s| s.boundary()))
456 }
457 }
458
459 struct Nested;
460 impl Component for Nested {
461 fn view(&self) -> RenderNode {
462 RenderNode::group([
463 rect(0.0),
464 RenderNode::group([rect(1.0), RenderNode::Empty, RenderNode::group([rect(2.0)])]),
465 rect(3.0),
466 ])
467 }
468 }
469
470 #[test]
471 fn flatten_nested_groups_and_empties() {
472 let root = SegmentRoot::mount(Nested);
473 assert_eq!(root.commands().len(), 4);
475 }
476
477 #[test]
478 fn composes_children_in_order() {
479 let a = signal(0.0f32);
480 let b = signal(100.0f32);
481 let (sa, sb) = (a.clone(), b.clone());
482 let children = vec![
483 Segment::mount(Leaf { x: sa }),
484 Segment::mount(Leaf { x: sb }),
485 ];
486 let root = SegmentRoot::mount(Parent { children });
487 assert_eq!(root.commands().len(), 4);
489 }
490
491 fn cmd_x(c: &DrawCommand) -> f32 {
492 match c {
493 DrawCommand::Rect { rect, .. } => rect.x,
494 _ => -1.0,
495 }
496 }
497
498 struct WithOverlay;
499 impl Component for WithOverlay {
500 fn view(&self) -> RenderNode {
501 RenderNode::group([rect(1.0), RenderNode::overlay([rect(2.0)]), rect(3.0)])
502 }
503 }
504
505 #[test]
506 fn overlay_hoists_to_end() {
507 let root = SegmentRoot::mount(WithOverlay);
508 let cmds = root.commands();
509 let xs: Vec<f32> = cmds.iter().map(cmd_x).collect();
510 assert_eq!(xs, vec![1.0, 3.0, 2.0]);
512 }
513
514 struct OverlayParent {
515 child: Rc<Segment>,
516 }
517 impl Component for OverlayParent {
518 fn view(&self) -> RenderNode {
519 RenderNode::group([rect(1.0), RenderNode::overlay([self.child.boundary()])])
520 }
521 }
522
523 #[test]
524 fn overlay_hoists_child_segment() {
525 let child = Segment::mount(Leaf { x: signal(9.0) }); let root = SegmentRoot::mount(OverlayParent { child });
528 let cmds = root.commands();
529 let xs: Vec<f32> = cmds.iter().map(cmd_x).collect();
530 assert_eq!(xs, vec![1.0, 9.0, 14.0]);
531 }
532
533 #[test]
534 fn child_change_updates_output_without_parent_rerun() {
535 let a = signal(0.0f32);
536 let sa = a.clone();
537 let children = vec![Segment::mount(Leaf { x: sa })];
538 let root = SegmentRoot::mount(Parent { children });
539 let g0 = root.generation();
540 let first_x = match &root.commands()[0] {
541 DrawCommand::Rect { rect, .. } => rect.x,
542 _ => unreachable!(),
543 };
544 assert_eq!(first_x, 0.0);
545
546 a.set(42.0);
547 assert_ne!(root.generation(), g0, "child change must bump generation");
548 let new_x = match &root.commands()[0] {
549 DrawCommand::Rect { rect, .. } => rect.x,
550 _ => unreachable!(),
551 };
552 assert_eq!(new_x, 42.0, "composed output reflects the child update");
553 }
554
555 struct MemoLeaf {
556 double: reactive_core::Memo<i32>,
557 }
558 impl Component for MemoLeaf {
559 fn view(&self) -> RenderNode {
560 rect(self.double.get() as f32)
561 }
562 }
563
564 #[test]
565 fn signal_dependent_segment_updates_with_runner_batching() {
566 use reactive_core::{begin_batch, end_batch};
567 let a = signal(0.0f32);
568 let sa = a.clone();
569 let root = SegmentRoot::mount(Leaf { x: sa });
570 assert_eq!(animated_rect_x(&root), 0.0);
571 begin_batch();
572 a.set(42.0);
573 end_batch();
574 begin_batch();
575 let mid = animated_rect_x(&root);
576 end_batch();
577 assert_eq!(
578 mid, 42.0,
579 "signal-reading segment must reflect the batched set"
580 );
581 }
582
583 #[test]
585 fn memo_dependent_segment_updates_with_runner_batching() {
586 use reactive_core::{begin_batch, end_batch, memo};
587 let count = signal(0i32);
588 let count_mv = count.clone();
589 let double = memo(move || count_mv.get() * 2);
590 let root = SegmentRoot::mount(MemoLeaf {
591 double: double.clone(),
592 });
593 assert_eq!(animated_rect_x(&root), 0.0);
594
595 begin_batch();
596 count.set(3);
597 end_batch();
598 begin_batch();
599 let mid = animated_rect_x(&root);
600 end_batch();
601 assert_eq!(
602 mid, 6.0,
603 "memo-reading segment must reflect the flushed memo"
604 );
605 }
606
607 struct ThemedButton {
610 theme: RwSignal<f32>,
611 sel: RwSignal<i32>,
612 }
613 impl Component for ThemedButton {
614 fn view(&self) -> RenderNode {
615 let c = self.theme.get(); self.sel.get(); RenderNode::rect(
618 Rect::new(0.0, 0.0, 10.0, 10.0),
619 RectStyle::default().with_fill(Color::rgba(c, c, c, 1.0)),
620 )
621 }
622 fn on_event(&mut self, _event: &platform_core::Event) -> crate::component::EventResult {
623 self.sel.update(|n| *n += 1); crate::component::EventResult::Handled
625 }
626 }
627
628 fn first_rect_r(root: &SegmentRoot) -> f32 {
629 match &root.commands()[0] {
630 DrawCommand::Rect { style, .. } => style.fill.unwrap().solid_color().r,
631 _ => unreachable!(),
632 }
633 }
634
635 #[test]
642 fn dispatch_must_be_batched_or_segment_drops_subscriptions() {
643 use reactive_core::{batch, signal};
644
645 {
647 let theme = signal(0.2f32);
648 let sel = signal(0i32);
649 let widget = Rc::new(RefCell::new(ThemedButton {
650 theme: theme.clone(),
651 sel: sel.clone(),
652 }));
653 let render = {
654 let w = Rc::clone(&widget);
655 move || w.try_borrow().ok().map(|c| c.view())
656 };
657 let root = SegmentRoot::from_segment(Segment::mount_fn_named("Component", render));
658 assert!((first_rect_r(&root) - 0.2).abs() < 1e-6);
659
660 widget
661 .borrow_mut()
662 .on_event(&platform_core::Event::CursorLeft); theme.set(0.9);
664 assert!(
665 (first_rect_r(&root) - 0.2).abs() < 1e-6,
666 "unbatched dispatch must drop the theme subscription (frozen at old value)"
667 );
668 }
669
670 {
672 let theme = signal(0.2f32);
673 let sel = signal(0i32);
674 let widget = Rc::new(RefCell::new(ThemedButton {
675 theme: theme.clone(),
676 sel: sel.clone(),
677 }));
678 let render = {
679 let w = Rc::clone(&widget);
680 move || w.try_borrow().ok().map(|c| c.view())
681 };
682 let root = SegmentRoot::from_segment(Segment::mount_fn_named("Component", render));
683 assert!((first_rect_r(&root) - 0.2).abs() < 1e-6);
684
685 batch(|| {
686 widget
687 .borrow_mut()
688 .on_event(&platform_core::Event::CursorLeft)
689 });
690 theme.set(0.9);
691 assert!(
692 (first_rect_r(&root) - 0.9).abs() < 1e-6,
693 "batched dispatch must preserve the theme subscription (tracks new value)"
694 );
695 }
696 }
697
698 struct AnimatedLeaf {
699 x: motion_core::Animated<f32>,
700 }
701 impl Component for AnimatedLeaf {
702 fn view(&self) -> RenderNode {
703 rect(self.x.get())
704 }
705 }
706
707 fn animated_rect_x(root: &SegmentRoot) -> f32 {
708 match &root.commands()[0] {
709 DrawCommand::Rect { rect, .. } => rect.x,
710 _ => unreachable!(),
711 }
712 }
713
714 #[test]
720 fn animated_get_reflects_tick_in_commands_and_settles() {
721 use std::time::{Duration, Instant};
722
723 motion_core::reset();
727 motion_core::set_scale(1.0);
728
729 let anim = motion_core::Animated::new(
730 0.0f32,
731 motion_core::tween(Duration::from_millis(100), motion_core::Easing::Linear),
732 );
733 let root = SegmentRoot::mount(AnimatedLeaf { x: anim.clone() });
734
735 assert_eq!(animated_rect_x(&root), 0.0);
737 let g0 = root.generation();
738
739 anim.retarget(10.0);
740 assert!(
741 motion_core::has_active(),
742 "retarget must register an active animation"
743 );
744
745 let base = Instant::now();
746 motion_core::tick(base);
748 assert_eq!(
749 root.generation(),
750 g0,
751 "the t0-establishing tick must not recompose"
752 );
753 assert_eq!(animated_rect_x(&root), 0.0);
754
755 motion_core::tick(base + Duration::from_millis(50));
760 let mid_x = animated_rect_x(&root);
761 let g1 = root.generation();
762 assert!(
763 (mid_x - 5.0).abs() < 1e-3,
764 "expected the midpoint of the tween, got {mid_x}"
765 );
766 assert_ne!(g1, g0, "an in-flight tick must bump the compose generation");
767
768 motion_core::tick(base + Duration::from_millis(100));
770 let end_x = animated_rect_x(&root);
771 let g2 = root.generation();
772 assert_eq!(end_x, 10.0);
773 assert_ne!(g2, g1, "the settling tick must still bump the generation");
774 assert!(
775 !motion_core::has_active(),
776 "a settled tween must deregister"
777 );
778
779 motion_core::tick(base + Duration::from_millis(200));
781 assert_eq!(animated_rect_x(&root), 10.0);
782 assert_eq!(
783 root.generation(),
784 g2,
785 "a tick with no active animations must not bump the generation"
786 );
787 }
788}