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 size = Size {
112        width: rect.width,
113        height: rect.height,
114    };
115
116    let behind = collect_primitives_from_commands(
117        node_id,
118        rect,
119        size,
120        data.modifier_slices().draw_commands(),
121        PaintLayer::Behind,
122    );
123    let overlay = collect_primitives_from_commands(
124        node_id,
125        rect,
126        size,
127        data.modifier_slices().draw_commands(),
128        PaintLayer::Overlay,
129    );
130    (behind, overlay)
131}
132
133fn collect_primitives_from_commands(
134    node_id: NodeId,
135    rect: Rect,
136    size: Size,
137    commands: &[ModifierDrawCommand],
138    layer: PaintLayer,
139) -> Vec<RenderOp> {
140    let split_with_content = |primitives: Vec<DrawPrimitive>, layer| {
141        let Some(last_content_idx) = primitives
142            .iter()
143            .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
144        else {
145            return if layer == PaintLayer::Overlay {
146                primitives
147                    .into_iter()
148                    .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
149                    .collect()
150            } else {
151                Vec::new()
152            };
153        };
154
155        primitives
156            .into_iter()
157            .enumerate()
158            .filter_map(|(index, primitive)| {
159                if matches!(primitive, DrawPrimitive::Content) {
160                    return None;
161                }
162                let is_before = index < last_content_idx;
163                match layer {
164                    PaintLayer::Behind if is_before => Some(primitive),
165                    PaintLayer::Overlay if !is_before => Some(primitive),
166                    _ => None,
167                }
168            })
169            .collect()
170    };
171
172    let mut ops = Vec::new();
173    for command in commands {
174        let primitives = match (layer, command) {
175            (PaintLayer::Behind, ModifierDrawCommand::Behind(func)) => func(size)
176                .into_iter()
177                .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
178                .collect(),
179            (PaintLayer::Overlay, ModifierDrawCommand::Overlay(func)) => func(size)
180                .into_iter()
181                .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
182                .collect(),
183            (PaintLayer::Behind | PaintLayer::Overlay, ModifierDrawCommand::WithContent(func)) => {
184                split_with_content(func(size), layer)
185            }
186            _ => Vec::new(),
187        };
188        for primitive in primitives {
189            ops.push(RenderOp::Primitive {
190                node_id,
191                layer,
192                primitive: translate_primitive(primitive, rect.x, rect.y),
193            });
194        }
195    }
196    ops
197}
198
199fn translate_primitive(primitive: DrawPrimitive, dx: f32, dy: f32) -> DrawPrimitive {
200    match primitive {
201        DrawPrimitive::Content => DrawPrimitive::Content,
202        DrawPrimitive::Blend {
203            primitive,
204            blend_mode,
205        } => DrawPrimitive::Blend {
206            primitive: Box::new(translate_primitive(*primitive, dx, dy)),
207            blend_mode,
208        },
209        DrawPrimitive::Rect { rect, brush } => DrawPrimitive::Rect {
210            rect: rect.translate(dx, dy),
211            brush,
212        },
213        DrawPrimitive::RoundRect { rect, brush, radii } => DrawPrimitive::RoundRect {
214            rect: rect.translate(dx, dy),
215            brush,
216            radii,
217        },
218        DrawPrimitive::Image {
219            rect,
220            image,
221            alpha,
222            color_filter,
223            sampling,
224            src_rect,
225        } => DrawPrimitive::Image {
226            rect: rect.translate(dx, dy),
227            image,
228            alpha,
229            color_filter,
230            sampling,
231            src_rect,
232        },
233        DrawPrimitive::Shadow(shadow) => {
234            use cranpose_ui_graphics::ShadowPrimitive;
235            DrawPrimitive::Shadow(match shadow {
236                ShadowPrimitive::Drop {
237                    shape,
238                    blur_radius,
239                    blend_mode,
240                } => ShadowPrimitive::Drop {
241                    shape: Box::new(translate_primitive(*shape, dx, dy)),
242                    blur_radius,
243                    blend_mode,
244                },
245                ShadowPrimitive::Inner {
246                    fill,
247                    cutout,
248                    blur_radius,
249                    blend_mode,
250                    clip_rect,
251                } => ShadowPrimitive::Inner {
252                    fill: Box::new(translate_primitive(*fill, dx, dy)),
253                    cutout: Box::new(translate_primitive(*cutout, dx, dy)),
254                    blur_radius,
255                    blend_mode,
256                    clip_rect: clip_rect.translate(dx, dy),
257                },
258            })
259        }
260    }
261}
262
263// ═══════════════════════════════════════════════════════════════════════════
264// Direct Applier Rendering (new architecture)
265// ═══════════════════════════════════════════════════════════════════════════
266
267impl HeadlessRenderer {
268    /// Renders the scene by traversing LayoutNodes directly via the Applier.
269    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
270    pub fn render_from_applier(
271        &self,
272        applier: &mut MemoryApplier,
273        root: NodeId,
274    ) -> RecordedRenderScene {
275        let mut operations = Vec::new();
276        self.render_node_from_applier(applier, root, Point::default(), &mut operations);
277        RecordedRenderScene::new(operations)
278    }
279
280    #[allow(clippy::only_used_in_recursion)]
281    fn render_node_from_applier(
282        &self,
283        applier: &mut MemoryApplier,
284        node_id: NodeId,
285        parent_offset: Point,
286        operations: &mut Vec<RenderOp>,
287    ) {
288        // Read layout state and node data from LayoutNode
289        let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
290            let state = node.layout_state();
291            let modifier_slices = node.modifier_slices_snapshot();
292            let children: Vec<NodeId> = node.children.clone();
293            (state, modifier_slices, children)
294        }) {
295            Ok(data) => data,
296            Err(_) => return, // Node not found or type mismatch
297        };
298
299        let (layout_state, modifier_slices, children) = node_data;
300
301        // Skip nodes that weren't placed
302        if !layout_state.is_placed {
303            return;
304        }
305
306        // Calculate absolute position
307        let abs_x = parent_offset.x + layout_state.position.x;
308        let abs_y = parent_offset.y + layout_state.position.y;
309
310        let rect = Rect {
311            x: abs_x,
312            y: abs_y,
313            width: layout_state.size.width,
314            height: layout_state.size.height,
315        };
316
317        let size = Size {
318            width: rect.width,
319            height: rect.height,
320        };
321
322        // Collect draw commands from modifier slices
323        let mut behind = Vec::new();
324        let mut overlay = Vec::new();
325        behind.extend(collect_primitives_from_commands(
326            node_id,
327            rect,
328            size,
329            modifier_slices.draw_commands(),
330            PaintLayer::Behind,
331        ));
332        overlay.extend(collect_primitives_from_commands(
333            node_id,
334            rect,
335            size,
336            modifier_slices.draw_commands(),
337            PaintLayer::Overlay,
338        ));
339
340        operations.append(&mut behind);
341
342        // Render text content if present
343        if let Some(text) = modifier_slices.text_content() {
344            operations.push(RenderOp::Text {
345                node_id,
346                rect,
347                value: text.to_string(),
348            });
349        }
350
351        // Calculate content offset for children (includes node position + content_offset from padding etc.)
352        let child_offset = Point {
353            x: abs_x + layout_state.content_offset.x,
354            y: abs_y + layout_state.content_offset.y,
355        };
356
357        // Render children
358        for child_id in children {
359            self.render_node_from_applier(applier, child_id, child_offset, operations);
360        }
361
362        operations.append(&mut overlay);
363    }
364}
365
366#[cfg(test)]
367#[path = "tests/renderer_tests.rs"]
368mod tests;