Skip to main content

telar_ui_tree/
tree.rs

1use std::cell::{Ref, RefCell};
2use std::rc::Rc;
3
4use platform_core::Event;
5use reactive_core::batch;
6use renderer_core::DrawCommand;
7
8use crate::component::{Component, EventResult};
9use crate::segment::{self, Segment, SegmentRoot};
10
11pub struct ComponentList {
12    // Shared with the root segment: the segment borrows it immutably to render; on_event borrows it mutably. They never overlap because event dispatch is batched (flush happens after on_event).
13    root: Rc<RefCell<dyn Component>>,
14    segment_root: SegmentRoot,
15}
16
17impl ComponentList {
18    pub fn new<C: Component + 'static>(component: C) -> Self {
19        let root: Rc<RefCell<dyn Component>> = Rc::new(RefCell::new(component));
20        let seg = Segment::mount_dyn(Rc::clone(&root));
21        Self {
22            root,
23            segment_root: SegmentRoot::from_segment(seg),
24        }
25    }
26
27    /// Current content generation. Increments whenever the composed draw commands are rebuilt. Two reads returning the same value guarantee identical `commands()` output.
28    pub fn generation(&self) -> u64 {
29        self.segment_root.generation()
30    }
31
32    pub fn is_dirty(&self) -> bool {
33        self.segment_root.is_dirty()
34    }
35
36    pub fn commands(&self) -> Ref<'_, Vec<DrawCommand>> {
37        self.segment_root.commands()
38    }
39
40    /// Emits the component tree in pre-order for the devtools inspector. See [`SegmentRoot::walk`].
41    pub fn walk_tree(&self, out: &mut Vec<segment::SegmentNodeInfo>) {
42        self.segment_root.walk(out);
43    }
44
45    pub fn on_event(&mut self, event: &Event) -> EventResult {
46        // Batch so any signals mutated by handlers flush their effects AFTER on_event returns (and releases the borrow_mut), never re-entering a segment effect mid-borrow.
47        // Overlay priority routing (blocking a modal's background) is NOT done here: it must run on the
48        // side that owns the overlay registry, which under hot reload is the app dylib, not the host that
49        // holds this `ComponentList`. The runner consults it via `App::dispatch_overlays` (bridged across
50        // the dylib boundary like `relayout`) before calling this, and skips this call when an overlay
51        // consumed the event. See `overlay_dispatch` and `crate::app::App::dispatch_overlays`.
52        batch(|| self.root.borrow_mut().on_event(event))
53    }
54
55    // In hot-reload mode the dylib's reactive signals are not tracked by the binary's effects, so state changes from on_event (e.g. WindowResized updating layout) would never trigger a re-render. Call this after on_event to force every segment's view effect to re-run so it reads fresh layout and state.
56    pub fn bump_force_ticks(&self) {
57        batch(segment::bump_force_ticks);
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use geometry_core::Rect;
64    use reactive_core::signal;
65    use renderer_core::{Color, RectStyle, ShapeStyle};
66    use std::sync::Arc;
67
68    use super::*;
69    use crate::render_node::RenderNode;
70
71    fn sample_rect(x: f32) -> DrawCommand {
72        DrawCommand::Rect {
73            rect: Rect::new(x, 0.0, 10.0, 10.0),
74            style: Arc::new(RectStyle::default().with_fill(Color::BLACK)),
75        }
76    }
77
78    struct Fixed;
79
80    impl Component for Fixed {
81        fn view(&self) -> RenderNode {
82            RenderNode::group([
83                RenderNode::Primitive(sample_rect(0.0)),
84                RenderNode::Primitive(sample_rect(20.0)),
85            ])
86        }
87    }
88
89    #[test]
90    fn tree_initial_render() {
91        let tree = ComponentList::new(Fixed);
92        let cmds = tree.commands();
93        assert_eq!(cmds.len(), 2);
94    }
95
96    struct Counter {
97        value: reactive_core::RwSignal<i32>,
98    }
99
100    impl Component for Counter {
101        fn view(&self) -> RenderNode {
102            let n = self.value.get();
103            RenderNode::group((0..n).map(|i| RenderNode::Primitive(sample_rect(i as f32 * 10.0))))
104        }
105    }
106
107    #[test]
108    fn tree_reactive_update() {
109        let signal = signal(2i32);
110        let tree = ComponentList::new(Counter {
111            value: signal.clone(),
112        });
113
114        assert_eq!(tree.commands().len(), 2);
115
116        signal.set(5);
117        assert_eq!(tree.commands().len(), 5);
118
119        signal.set(0);
120        assert_eq!(tree.commands().len(), 0);
121    }
122}