Skip to main content

cranpose_ui/
renderer.rs

1use crate::layout::{LayoutBox, LayoutNodeData, LayoutTree};
2use crate::modifier::{DrawCommand as ModifierDrawCommand, Point, Rect, Size};
3use crate::widgets::LayoutNode;
4use cranpose_core::{MemoryApplier, NodeId};
5use cranpose_ui_graphics::DrawPrimitive;
6
7/// Layer that a paint operation targets within the rendering pipeline.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum PaintLayer {
10    Behind,
11    Content,
12    Overlay,
13}
14
15/// A rendered operation emitted by the headless renderer stub.
16#[derive(Clone, Debug, PartialEq)]
17pub enum RenderOp {
18    Primitive {
19        node_id: NodeId,
20        layer: PaintLayer,
21        primitive: DrawPrimitive,
22    },
23    Text {
24        node_id: NodeId,
25        rect: Rect,
26        value: String,
27    },
28}
29
30/// A collection of render operations for a composed scene.
31#[derive(Clone, Debug, Default, PartialEq)]
32pub struct RecordedRenderScene {
33    operations: Vec<RenderOp>,
34}
35
36impl RecordedRenderScene {
37    pub fn new(operations: Vec<RenderOp>) -> Self {
38        Self { operations }
39    }
40
41    /// Returns a slice of recorded render operations in submission order.
42    pub fn operations(&self) -> &[RenderOp] {
43        &self.operations
44    }
45
46    /// Consumes the scene and yields the owned operations.
47    pub fn into_operations(self) -> Vec<RenderOp> {
48        self.operations
49    }
50
51    /// Returns an iterator over primitives that target the provided paint layer.
52    pub fn primitives_for(&self, layer: PaintLayer) -> impl Iterator<Item = &DrawPrimitive> {
53        self.operations.iter().filter_map(move |op| match op {
54            RenderOp::Primitive {
55                layer: op_layer,
56                primitive,
57                ..
58            } if *op_layer == layer => Some(primitive),
59            _ => None,
60        })
61    }
62}
63
64/// A lightweight renderer that walks the layout tree and materialises paint commands.
65#[derive(Default)]
66pub struct HeadlessRenderer;
67
68impl HeadlessRenderer {
69    pub fn new() -> Self {
70        Self
71    }
72
73    pub fn render(&self, tree: &LayoutTree) -> RecordedRenderScene {
74        let mut operations = Vec::new();
75        self.render_box(tree.root(), &mut operations);
76        RecordedRenderScene::new(operations)
77    }
78
79    #[allow(clippy::only_used_in_recursion)]
80    fn render_box(&self, layout: &LayoutBox, operations: &mut Vec<RenderOp>) {
81        let rect = layout.rect;
82        let (mut behind, mut overlay) = evaluate_modifier(layout.node_id, &layout.node_data, rect);
83
84        operations.append(&mut behind);
85
86        // Render text content if present in modifier slices.
87        // This follows Jetpack Compose's pattern where text is a modifier node capability
88        // (TextModifierNode implements LayoutModifierNode + DrawModifierNode + SemanticsNode)
89        if let Some(text) = layout.node_data.modifier_slices().text_content() {
90            operations.push(RenderOp::Text {
91                node_id: layout.node_id,
92                rect,
93                value: text.to_string(),
94            });
95        }
96
97        // Render children
98        for child in &layout.children {
99            self.render_box(child, operations);
100        }
101
102        operations.append(&mut overlay);
103    }
104}
105
106fn evaluate_modifier(
107    node_id: NodeId,
108    data: &LayoutNodeData,
109    rect: Rect,
110) -> (Vec<RenderOp>, Vec<RenderOp>) {
111    let mut behind = Vec::new();
112    let mut overlay = Vec::new();
113
114    let size = Size {
115        width: rect.width,
116        height: rect.height,
117    };
118
119    // Render via modifier slices - all drawing now goes through draw commands
120    // collected from the modifier node chain, including backgrounds, borders, etc.
121    for command in data.modifier_slices().draw_commands() {
122        match command {
123            ModifierDrawCommand::Behind(func) => {
124                for primitive in func(size) {
125                    behind.push(RenderOp::Primitive {
126                        node_id,
127                        layer: PaintLayer::Behind,
128                        primitive: translate_primitive(primitive, rect.x, rect.y),
129                    });
130                }
131            }
132            ModifierDrawCommand::Overlay(func) => {
133                for primitive in func(size) {
134                    overlay.push(RenderOp::Primitive {
135                        node_id,
136                        layer: PaintLayer::Overlay,
137                        primitive: translate_primitive(primitive, rect.x, rect.y),
138                    });
139                }
140            }
141        }
142    }
143
144    (behind, overlay)
145}
146
147fn translate_primitive(primitive: DrawPrimitive, dx: f32, dy: f32) -> DrawPrimitive {
148    match primitive {
149        DrawPrimitive::Rect { rect, brush } => DrawPrimitive::Rect {
150            rect: rect.translate(dx, dy),
151            brush,
152        },
153        DrawPrimitive::RoundRect { rect, brush, radii } => DrawPrimitive::RoundRect {
154            rect: rect.translate(dx, dy),
155            brush,
156            radii,
157        },
158    }
159}
160
161// ═══════════════════════════════════════════════════════════════════════════
162// Direct Applier Rendering (new architecture)
163// ═══════════════════════════════════════════════════════════════════════════
164
165impl HeadlessRenderer {
166    /// Renders the scene by traversing LayoutNodes directly via the Applier.
167    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
168    pub fn render_from_applier(
169        &self,
170        applier: &mut MemoryApplier,
171        root: NodeId,
172    ) -> RecordedRenderScene {
173        let mut operations = Vec::new();
174        self.render_node_from_applier(applier, root, Point::default(), &mut operations);
175        RecordedRenderScene::new(operations)
176    }
177
178    #[allow(clippy::only_used_in_recursion)]
179    fn render_node_from_applier(
180        &self,
181        applier: &mut MemoryApplier,
182        node_id: NodeId,
183        parent_offset: Point,
184        operations: &mut Vec<RenderOp>,
185    ) {
186        // Read layout state and node data from LayoutNode
187        let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
188            let state = node.layout_state();
189            let modifier_slices = node.modifier_slices_snapshot();
190            let children: Vec<NodeId> = node.children.iter().copied().collect();
191            (state, modifier_slices, children)
192        }) {
193            Ok(data) => data,
194            Err(_) => return, // Node not found or type mismatch
195        };
196
197        let (layout_state, modifier_slices, children) = node_data;
198
199        // Skip nodes that weren't placed
200        if !layout_state.is_placed {
201            return;
202        }
203
204        // Calculate absolute position
205        let abs_x = parent_offset.x + layout_state.position.x;
206        let abs_y = parent_offset.y + layout_state.position.y;
207
208        let rect = Rect {
209            x: abs_x,
210            y: abs_y,
211            width: layout_state.size.width,
212            height: layout_state.size.height,
213        };
214
215        let size = Size {
216            width: rect.width,
217            height: rect.height,
218        };
219
220        // Collect draw commands from modifier slices
221        let mut behind = Vec::new();
222        let mut overlay = Vec::new();
223
224        for command in modifier_slices.draw_commands() {
225            match command {
226                ModifierDrawCommand::Behind(func) => {
227                    for primitive in func(size) {
228                        behind.push(RenderOp::Primitive {
229                            node_id,
230                            layer: PaintLayer::Behind,
231                            primitive: translate_primitive(primitive, rect.x, rect.y),
232                        });
233                    }
234                }
235                ModifierDrawCommand::Overlay(func) => {
236                    for primitive in func(size) {
237                        overlay.push(RenderOp::Primitive {
238                            node_id,
239                            layer: PaintLayer::Overlay,
240                            primitive: translate_primitive(primitive, rect.x, rect.y),
241                        });
242                    }
243                }
244            }
245        }
246
247        operations.append(&mut behind);
248
249        // Render text content if present
250        if let Some(text) = modifier_slices.text_content() {
251            operations.push(RenderOp::Text {
252                node_id,
253                rect,
254                value: text.to_string(),
255            });
256        }
257
258        // Calculate content offset for children (includes node position + content_offset from padding etc.)
259        let child_offset = Point {
260            x: abs_x + layout_state.content_offset.x,
261            y: abs_y + layout_state.content_offset.y,
262        };
263
264        // Render children
265        for child_id in children {
266            self.render_node_from_applier(applier, child_id, child_offset, operations);
267        }
268
269        operations.append(&mut overlay);
270    }
271}
272
273#[cfg(test)]
274#[path = "tests/renderer_tests.rs"]
275mod tests;