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        DrawPrimitive::Image {
159            rect,
160            image,
161            alpha,
162            color_filter,
163            src_rect,
164        } => DrawPrimitive::Image {
165            rect: rect.translate(dx, dy),
166            image,
167            alpha,
168            color_filter,
169            src_rect,
170        },
171    }
172}
173
174// ═══════════════════════════════════════════════════════════════════════════
175// Direct Applier Rendering (new architecture)
176// ═══════════════════════════════════════════════════════════════════════════
177
178impl HeadlessRenderer {
179    /// Renders the scene by traversing LayoutNodes directly via the Applier.
180    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
181    pub fn render_from_applier(
182        &self,
183        applier: &mut MemoryApplier,
184        root: NodeId,
185    ) -> RecordedRenderScene {
186        let mut operations = Vec::new();
187        self.render_node_from_applier(applier, root, Point::default(), &mut operations);
188        RecordedRenderScene::new(operations)
189    }
190
191    #[allow(clippy::only_used_in_recursion)]
192    fn render_node_from_applier(
193        &self,
194        applier: &mut MemoryApplier,
195        node_id: NodeId,
196        parent_offset: Point,
197        operations: &mut Vec<RenderOp>,
198    ) {
199        // Read layout state and node data from LayoutNode
200        let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
201            let state = node.layout_state();
202            let modifier_slices = node.modifier_slices_snapshot();
203            let children: Vec<NodeId> = node.children.iter().copied().collect();
204            (state, modifier_slices, children)
205        }) {
206            Ok(data) => data,
207            Err(_) => return, // Node not found or type mismatch
208        };
209
210        let (layout_state, modifier_slices, children) = node_data;
211
212        // Skip nodes that weren't placed
213        if !layout_state.is_placed {
214            return;
215        }
216
217        // Calculate absolute position
218        let abs_x = parent_offset.x + layout_state.position.x;
219        let abs_y = parent_offset.y + layout_state.position.y;
220
221        let rect = Rect {
222            x: abs_x,
223            y: abs_y,
224            width: layout_state.size.width,
225            height: layout_state.size.height,
226        };
227
228        let size = Size {
229            width: rect.width,
230            height: rect.height,
231        };
232
233        // Collect draw commands from modifier slices
234        let mut behind = Vec::new();
235        let mut overlay = Vec::new();
236
237        for command in modifier_slices.draw_commands() {
238            match command {
239                ModifierDrawCommand::Behind(func) => {
240                    for primitive in func(size) {
241                        behind.push(RenderOp::Primitive {
242                            node_id,
243                            layer: PaintLayer::Behind,
244                            primitive: translate_primitive(primitive, rect.x, rect.y),
245                        });
246                    }
247                }
248                ModifierDrawCommand::Overlay(func) => {
249                    for primitive in func(size) {
250                        overlay.push(RenderOp::Primitive {
251                            node_id,
252                            layer: PaintLayer::Overlay,
253                            primitive: translate_primitive(primitive, rect.x, rect.y),
254                        });
255                    }
256                }
257            }
258        }
259
260        operations.append(&mut behind);
261
262        // Render text content if present
263        if let Some(text) = modifier_slices.text_content() {
264            operations.push(RenderOp::Text {
265                node_id,
266                rect,
267                value: text.to_string(),
268            });
269        }
270
271        // Calculate content offset for children (includes node position + content_offset from padding etc.)
272        let child_offset = Point {
273            x: abs_x + layout_state.content_offset.x,
274            y: abs_y + layout_state.content_offset.y,
275        };
276
277        // Render children
278        for child_id in children {
279            self.render_node_from_applier(applier, child_id, child_offset, operations);
280        }
281
282        operations.append(&mut overlay);
283    }
284}
285
286#[cfg(test)]
287#[path = "tests/renderer_tests.rs"]
288mod tests;