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>>>,
62 own_overlay: Rc<RefCell<Vec<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 let name = component.debug_name();
100 let component = Rc::new(RefCell::new(component));
101 Self::mount_fn_named(name, move || component.try_borrow().ok().map(|c| c.view()))
102 }
103
104 pub fn mount_dyn(component: Rc<RefCell<dyn Component>>) -> Rc<Segment> {
109 let name = component
110 .try_borrow()
111 .map(|c| c.debug_name())
112 .unwrap_or("Component");
113 Self::mount_fn_named(name, move || component.try_borrow().ok().map(|c| c.view()))
114 }
115
116 pub fn mount_fn(render: impl Fn() -> Option<RenderNode> + 'static) -> Rc<Segment> {
121 Self::mount_fn_named("Component", render)
122 }
123
124 pub fn mount_fn_named(
126 name: &'static str,
127 render: impl Fn() -> Option<RenderNode> + 'static,
128 ) -> Rc<Segment> {
129 let own_commands: Rc<RefCell<Vec<DrawCommand>>> = Default::default();
130 let own_overlay: Rc<RefCell<Vec<bool>>> = Default::default();
131 let child_slots: Rc<RefCell<ChildSlots>> = Default::default();
132 let stack: Rc<RefCell<Vec<Step>>> = Default::default();
133 let is_dirty = Rc::new(Cell::new(true));
135
136 let own_c = Rc::clone(&own_commands);
137 let overlay_c = Rc::clone(&own_overlay);
138 let slots_c = Rc::clone(&child_slots);
139 let dirty_c = Rc::clone(&is_dirty);
140 let _effect = effect(move || {
141 force_tick().get(); let Some(node) = render() else {
143 return; };
145 let mut own = own_c.borrow_mut();
146 let mut overlay = overlay_c.borrow_mut();
147 let mut stk = stack.borrow_mut();
148 let mut new_slots: ChildSlots = Vec::new();
149 let own_changed =
150 flatten_segment(node, &mut own, &mut overlay, &mut new_slots, &mut stk);
151 drop(stk);
152 drop(own);
153 drop(overlay);
154 let mut slots = slots_c.borrow_mut();
155 let slots_changed = slots.len() != new_slots.len()
157 || slots
158 .iter()
159 .zip(new_slots.iter())
160 .any(|(a, b)| a.0 != b.0 || a.2 != b.2 || !Rc::ptr_eq(&a.1, &b.1));
161 if own_changed || slots_changed {
162 *slots = new_slots;
163 dirty_c.set(true);
164 }
165 });
166
167 Rc::new(Segment {
168 name,
169 own_commands,
170 own_overlay,
171 child_slots,
172 is_dirty,
173 _effect,
174 })
175 }
176
177 pub fn boundary(self: &Rc<Self>) -> RenderNode {
179 RenderNode::Boundary {
180 child: Rc::clone(self),
181 }
182 }
183
184 pub fn name(&self) -> &'static str {
186 self.name
187 }
188
189 pub fn walk(&self, out: &mut Vec<SegmentNodeInfo>) {
192 self.collect(0, out);
193 }
194
195 fn collect(&self, depth: usize, out: &mut Vec<SegmentNodeInfo>) -> Rect {
200 let idx = out.len();
201 out.push(SegmentNodeInfo {
203 id: idx as u64,
204 name: self.name,
205 depth,
206 rect: Rect::default(),
207 });
208
209 let mut bounds = Rect::default();
210 for cmd in self.own_commands.borrow().iter() {
211 let rect = match cmd {
212 DrawCommand::Rect { rect, .. } => *rect,
213 DrawCommand::Text { rect, .. } => *rect,
214 DrawCommand::Image { rect, .. } => *rect,
215 DrawCommand::PushClip { rect, .. } => *rect,
216 _ => continue,
217 };
218 bounds = union_nonempty(bounds, rect);
219 }
220
221 for (_, child, _) in self.child_slots.borrow().iter() {
222 bounds = union_nonempty(bounds, child.collect(depth + 1, out));
223 }
224
225 out[idx].rect = bounds;
226 bounds
227 }
228}
229
230fn flatten_segment(
233 root: RenderNode,
234 out: &mut Vec<DrawCommand>,
235 overlay: &mut Vec<bool>,
236 slots: &mut ChildSlots,
237 stack: &mut Vec<Step>,
238) -> bool {
239 stack.clear();
240 stack.push(Step::Node(root));
241 let mut pos: usize = 0;
242 let mut changed = false;
243 let mut overlay_depth: usize = 0;
245 let mut new_overlay: Vec<bool> = Vec::with_capacity(out.len());
248
249 macro_rules! emit_command {
250 ($command:expr) => {{
251 let command = $command;
252 if pos < out.len() {
253 if out[pos] != command {
254 out[pos] = command;
255 changed = true;
256 }
257 } else {
258 out.push(command);
259 changed = true;
260 }
261 new_overlay.push(overlay_depth > 0);
262 pos += 1;
263 }};
264 }
265
266 while let Some(step) = stack.pop() {
267 let node = match step {
268 Step::EndOverlay => {
269 overlay_depth -= 1;
270 continue;
271 }
272 Step::Node(node) => node,
273 };
274 match node {
275 RenderNode::Empty => {}
276 RenderNode::Primitive(cmd) => emit_command!(cmd),
277 RenderNode::Group { children } => {
278 for child in children.into_iter().rev() {
279 stack.push(Step::Node(child));
280 }
281 }
282 RenderNode::Transform { matrix, children } => {
283 stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopMatrix)));
284 for child in children.into_iter().rev() {
285 stack.push(Step::Node(child));
286 }
287 emit_command!(DrawCommand::PushMatrix { matrix });
288 }
289 RenderNode::Clip {
290 rect,
291 radius,
292 children,
293 } => {
294 stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopClip)));
295 for child in children.into_iter().rev() {
296 stack.push(Step::Node(child));
297 }
298 emit_command!(DrawCommand::PushClip { rect, radius });
299 }
300 RenderNode::Layer {
301 opacity,
302 backdrop_blur,
303 children,
304 } => {
305 stack.push(Step::Node(RenderNode::Primitive(DrawCommand::PopLayer)));
306 for child in children.into_iter().rev() {
307 stack.push(Step::Node(child));
308 }
309 emit_command!(DrawCommand::PushLayer {
310 opacity,
311 backdrop_blur
312 });
313 }
314 RenderNode::Overlay { children } => {
316 overlay_depth += 1;
317 stack.push(Step::EndOverlay);
318 for child in children.into_iter().rev() {
319 stack.push(Step::Node(child));
320 }
321 }
322 RenderNode::Boundary { child } => slots.push((pos, child, overlay_depth > 0)),
325 }
326 }
327
328 if pos != out.len() {
329 out.truncate(pos);
330 changed = true;
331 }
332 if *overlay != new_overlay {
333 *overlay = new_overlay;
334 changed = true;
335 }
336 changed
337}
338
339pub(crate) fn compose_into(
347 seg: &Segment,
348 out: &mut Vec<DrawCommand>,
349 overlay_out: &mut Vec<DrawCommand>,
350 in_overlay: bool,
351) {
352 seg.is_dirty.set(false);
353 let own_commands = seg.own_commands.borrow();
354 let own_overlay = seg.own_overlay.borrow();
355 let slots = seg.child_slots.borrow();
356 let mut si = 0;
357 for (i, cmd) in own_commands.iter().enumerate() {
358 while si < slots.len() && slots[si].0 == i {
359 compose_into(&slots[si].1, out, overlay_out, in_overlay || slots[si].2);
360 si += 1;
361 }
362 if in_overlay || own_overlay.get(i).copied().unwrap_or(false) {
363 overlay_out.push(cmd.clone());
364 } else {
365 out.push(cmd.clone());
366 }
367 }
368 while si < slots.len() {
369 compose_into(&slots[si].1, out, overlay_out, in_overlay || slots[si].2);
370 si += 1;
371 }
372}
373
374fn any_dirty(seg: &Segment) -> bool {
377 if seg.is_dirty.get() {
378 return true;
379 }
380 seg.child_slots
381 .borrow()
382 .iter()
383 .any(|(_, child, _)| any_dirty(child))
384}
385
386pub struct SegmentRoot {
390 root: Rc<Segment>,
391 cached: RefCell<Vec<DrawCommand>>,
392 compose_generation: Cell<u64>,
394 cache_valid: Cell<bool>,
395}
396
397impl SegmentRoot {
398 pub fn mount<C: Component + 'static>(component: C) -> Self {
399 Self::from_segment(Segment::mount(component))
400 }
401
402 pub fn from_segment(root: Rc<Segment>) -> Self {
403 SegmentRoot {
404 root,
405 cached: RefCell::new(Vec::new()),
406 compose_generation: Cell::new(0),
407 cache_valid: Cell::new(false),
408 }
409 }
410
411 pub fn generation(&self) -> u64 {
412 self.compose_generation.get()
413 }
414
415 pub fn walk(&self, out: &mut Vec<SegmentNodeInfo>) {
417 self.root.walk(out);
418 }
419
420 pub fn is_dirty(&self) -> bool {
422 !self.cache_valid.get() || any_dirty(&self.root)
423 }
424
425 pub fn commands(&self) -> Ref<'_, Vec<DrawCommand>> {
426 if !self.cache_valid.get() || any_dirty(&self.root) {
427 let mut cached = self.cached.borrow_mut();
428 cached.clear();
429 let mut overlay: Vec<DrawCommand> = Vec::new();
432 compose_into(&self.root, &mut cached, &mut overlay, false); cached.extend(overlay);
434 drop(cached);
435 self.compose_generation
436 .set(self.compose_generation.get().wrapping_add(1));
437 self.cache_valid.set(true);
438 }
439 self.cached.borrow()
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use geometry_core::Rect;
446 use reactive_core::{RwSignal, signal};
447 use renderer_core::{Color, RectStyle, ShapeStyle};
448
449 use super::*;
450
451 fn rect(x: f32) -> RenderNode {
452 RenderNode::rect(
453 Rect::new(x, 0.0, 10.0, 10.0),
454 RectStyle::default().with_fill(Color::BLACK),
455 )
456 }
457
458 struct Leaf {
459 x: RwSignal<f32>,
460 }
461 impl Component for Leaf {
462 fn view(&self) -> RenderNode {
463 RenderNode::group([rect(self.x.get()), rect(self.x.get() + 5.0)])
464 }
465 }
466
467 struct Parent {
468 children: Vec<Rc<Segment>>,
469 }
470 impl Component for Parent {
471 fn view(&self) -> RenderNode {
472 RenderNode::group(self.children.iter().map(|s| s.boundary()))
473 }
474 }
475
476 struct Nested;
477 impl Component for Nested {
478 fn view(&self) -> RenderNode {
479 RenderNode::group([
480 rect(0.0),
481 RenderNode::group([rect(1.0), RenderNode::Empty, RenderNode::group([rect(2.0)])]),
482 rect(3.0),
483 ])
484 }
485 }
486
487 #[test]
488 fn flatten_nested_groups_and_empties() {
489 let root = SegmentRoot::mount(Nested);
490 assert_eq!(root.commands().len(), 4);
492 }
493
494 #[test]
495 fn composes_children_in_order() {
496 let a = signal(0.0f32);
497 let b = signal(100.0f32);
498 let (sa, sb) = (a.clone(), b.clone());
499 let children = vec![
500 Segment::mount(Leaf { x: sa }),
501 Segment::mount(Leaf { x: sb }),
502 ];
503 let root = SegmentRoot::mount(Parent { children });
504 assert_eq!(root.commands().len(), 4);
506 }
507
508 fn cmd_x(c: &DrawCommand) -> f32 {
509 match c {
510 DrawCommand::Rect { rect, .. } => rect.x,
511 _ => -1.0,
512 }
513 }
514
515 struct WithOverlay;
516 impl Component for WithOverlay {
517 fn view(&self) -> RenderNode {
518 RenderNode::group([rect(1.0), RenderNode::overlay([rect(2.0)]), rect(3.0)])
519 }
520 }
521
522 #[test]
523 fn overlay_hoists_to_end() {
524 let root = SegmentRoot::mount(WithOverlay);
525 let cmds = root.commands();
526 let xs: Vec<f32> = cmds.iter().map(cmd_x).collect();
527 assert_eq!(xs, vec![1.0, 3.0, 2.0]);
529 }
530
531 struct OverlayParent {
532 child: Rc<Segment>,
533 }
534 impl Component for OverlayParent {
535 fn view(&self) -> RenderNode {
536 RenderNode::group([rect(1.0), RenderNode::overlay([self.child.boundary()])])
537 }
538 }
539
540 #[test]
541 fn overlay_hoists_child_segment() {
542 let child = Segment::mount(Leaf { x: signal(9.0) }); let root = SegmentRoot::mount(OverlayParent { child });
545 let cmds = root.commands();
546 let xs: Vec<f32> = cmds.iter().map(cmd_x).collect();
547 assert_eq!(xs, vec![1.0, 9.0, 14.0]);
548 }
549
550 #[test]
551 fn child_change_updates_output_without_parent_rerun() {
552 let a = signal(0.0f32);
553 let sa = a.clone();
554 let children = vec![Segment::mount(Leaf { x: sa })];
555 let root = SegmentRoot::mount(Parent { children });
556 let g0 = root.generation();
557 let first_x = match &root.commands()[0] {
558 DrawCommand::Rect { rect, .. } => rect.x,
559 _ => unreachable!(),
560 };
561 assert_eq!(first_x, 0.0);
562
563 a.set(42.0);
564 assert_ne!(root.generation(), g0, "child change must bump generation");
565 let new_x = match &root.commands()[0] {
566 DrawCommand::Rect { rect, .. } => rect.x,
567 _ => unreachable!(),
568 };
569 assert_eq!(new_x, 42.0, "composed output reflects the child update");
570 }
571
572 struct MemoLeaf {
573 double: reactive_core::Memo<i32>,
574 }
575 impl Component for MemoLeaf {
576 fn view(&self) -> RenderNode {
577 rect(self.double.get() as f32)
578 }
579 }
580
581 #[test]
582 fn signal_dependent_segment_updates_with_runner_batching() {
583 use reactive_core::{begin_batch, end_batch};
584 let a = signal(0.0f32);
585 let sa = a.clone();
586 let root = SegmentRoot::mount(Leaf { x: sa });
587 assert_eq!(animated_rect_x(&root), 0.0);
588 begin_batch();
589 a.set(42.0);
590 end_batch();
591 begin_batch();
592 let mid = animated_rect_x(&root);
593 end_batch();
594 assert_eq!(
595 mid, 42.0,
596 "signal-reading segment must reflect the batched set"
597 );
598 }
599
600 #[test]
602 fn memo_dependent_segment_updates_with_runner_batching() {
603 use reactive_core::{begin_batch, end_batch, memo};
604 let count = signal(0i32);
605 let count_mv = count.clone();
606 let double = memo(move || count_mv.get() * 2);
607 let root = SegmentRoot::mount(MemoLeaf {
608 double: double.clone(),
609 });
610 assert_eq!(animated_rect_x(&root), 0.0);
611
612 begin_batch();
613 count.set(3);
614 end_batch();
615 begin_batch();
616 let mid = animated_rect_x(&root);
617 end_batch();
618 assert_eq!(
619 mid, 6.0,
620 "memo-reading segment must reflect the flushed memo"
621 );
622 }
623
624 struct ThemedButton {
627 theme: RwSignal<f32>,
628 sel: RwSignal<i32>,
629 }
630 impl Component for ThemedButton {
631 fn view(&self) -> RenderNode {
632 let c = self.theme.get(); self.sel.get(); RenderNode::rect(
635 Rect::new(0.0, 0.0, 10.0, 10.0),
636 RectStyle::default().with_fill(Color::rgba(c, c, c, 1.0)),
637 )
638 }
639 fn on_event(&mut self, _event: &platform_core::Event) -> crate::component::EventResult {
640 self.sel.update(|n| *n += 1); crate::component::EventResult::Handled
642 }
643 }
644
645 fn first_rect_r(root: &SegmentRoot) -> f32 {
646 match &root.commands()[0] {
647 DrawCommand::Rect { style, .. } => style.fill.unwrap().solid_color().r,
648 _ => unreachable!(),
649 }
650 }
651
652 #[test]
659 fn dispatch_must_be_batched_or_segment_drops_subscriptions() {
660 use reactive_core::{batch, signal};
661
662 {
664 let theme = signal(0.2f32);
665 let sel = signal(0i32);
666 let widget = Rc::new(RefCell::new(ThemedButton {
667 theme: theme.clone(),
668 sel: sel.clone(),
669 }));
670 let render = {
671 let w = Rc::clone(&widget);
672 move || w.try_borrow().ok().map(|c| c.view())
673 };
674 let root = SegmentRoot::from_segment(Segment::mount_fn(render));
675 assert!((first_rect_r(&root) - 0.2).abs() < 1e-6);
676
677 widget
678 .borrow_mut()
679 .on_event(&platform_core::Event::CursorLeft); theme.set(0.9);
681 assert!(
682 (first_rect_r(&root) - 0.2).abs() < 1e-6,
683 "unbatched dispatch must drop the theme subscription (frozen at old value)"
684 );
685 }
686
687 {
689 let theme = signal(0.2f32);
690 let sel = signal(0i32);
691 let widget = Rc::new(RefCell::new(ThemedButton {
692 theme: theme.clone(),
693 sel: sel.clone(),
694 }));
695 let render = {
696 let w = Rc::clone(&widget);
697 move || w.try_borrow().ok().map(|c| c.view())
698 };
699 let root = SegmentRoot::from_segment(Segment::mount_fn(render));
700 assert!((first_rect_r(&root) - 0.2).abs() < 1e-6);
701
702 batch(|| {
703 widget
704 .borrow_mut()
705 .on_event(&platform_core::Event::CursorLeft)
706 });
707 theme.set(0.9);
708 assert!(
709 (first_rect_r(&root) - 0.9).abs() < 1e-6,
710 "batched dispatch must preserve the theme subscription (tracks new value)"
711 );
712 }
713 }
714
715 struct AnimatedLeaf {
716 x: motion_core::Animated<f32>,
717 }
718 impl Component for AnimatedLeaf {
719 fn view(&self) -> RenderNode {
720 rect(self.x.get())
721 }
722 }
723
724 fn animated_rect_x(root: &SegmentRoot) -> f32 {
725 match &root.commands()[0] {
726 DrawCommand::Rect { rect, .. } => rect.x,
727 _ => unreachable!(),
728 }
729 }
730
731 #[test]
737 fn animated_get_reflects_tick_in_commands_and_settles() {
738 use std::time::{Duration, Instant};
739
740 motion_core::reset();
744 motion_core::set_scale(1.0);
745
746 let anim = motion_core::Animated::new(
747 0.0f32,
748 motion_core::tween(Duration::from_millis(100), motion_core::Easing::Linear),
749 );
750 let root = SegmentRoot::mount(AnimatedLeaf { x: anim.clone() });
751
752 assert_eq!(animated_rect_x(&root), 0.0);
754 let g0 = root.generation();
755
756 anim.retarget(10.0);
757 assert!(
758 motion_core::has_active(),
759 "retarget must register an active animation"
760 );
761
762 let base = Instant::now();
763 motion_core::tick(base);
765 assert_eq!(
766 root.generation(),
767 g0,
768 "the t0-establishing tick must not recompose"
769 );
770 assert_eq!(animated_rect_x(&root), 0.0);
771
772 motion_core::tick(base + Duration::from_millis(50));
777 let mid_x = animated_rect_x(&root);
778 let g1 = root.generation();
779 assert!(
780 (mid_x - 5.0).abs() < 1e-3,
781 "expected the midpoint of the tween, got {mid_x}"
782 );
783 assert_ne!(g1, g0, "an in-flight tick must bump the compose generation");
784
785 motion_core::tick(base + Duration::from_millis(100));
787 let end_x = animated_rect_x(&root);
788 let g2 = root.generation();
789 assert_eq!(end_x, 10.0);
790 assert_ne!(g2, g1, "the settling tick must still bump the generation");
791 assert!(
792 !motion_core::has_active(),
793 "a settled tween must deregister"
794 );
795
796 motion_core::tick(base + Duration::from_millis(200));
798 assert_eq!(animated_rect_x(&root), 10.0);
799 assert_eq!(
800 root.generation(),
801 g2,
802 "a tick with no active animations must not bump the generation"
803 );
804 }
805}