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            src_rect,
224        } => DrawPrimitive::Image {
225            rect: rect.translate(dx, dy),
226            image,
227            alpha,
228            color_filter,
229            src_rect,
230        },
231        DrawPrimitive::Shadow(shadow) => {
232            use cranpose_ui_graphics::ShadowPrimitive;
233            DrawPrimitive::Shadow(match shadow {
234                ShadowPrimitive::Drop {
235                    shape,
236                    blur_radius,
237                    blend_mode,
238                } => ShadowPrimitive::Drop {
239                    shape: Box::new(translate_primitive(*shape, dx, dy)),
240                    blur_radius,
241                    blend_mode,
242                },
243                ShadowPrimitive::Inner {
244                    fill,
245                    cutout,
246                    blur_radius,
247                    blend_mode,
248                    clip_rect,
249                } => ShadowPrimitive::Inner {
250                    fill: Box::new(translate_primitive(*fill, dx, dy)),
251                    cutout: Box::new(translate_primitive(*cutout, dx, dy)),
252                    blur_radius,
253                    blend_mode,
254                    clip_rect: clip_rect.translate(dx, dy),
255                },
256            })
257        }
258    }
259}
260
261// ═══════════════════════════════════════════════════════════════════════════
262// Direct Applier Rendering (new architecture)
263// ═══════════════════════════════════════════════════════════════════════════
264
265impl HeadlessRenderer {
266    /// Renders the scene by traversing LayoutNodes directly via the Applier.
267    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
268    pub fn render_from_applier(
269        &self,
270        applier: &mut MemoryApplier,
271        root: NodeId,
272    ) -> RecordedRenderScene {
273        let mut operations = Vec::new();
274        self.render_node_from_applier(applier, root, Point::default(), &mut operations);
275        RecordedRenderScene::new(operations)
276    }
277
278    #[allow(clippy::only_used_in_recursion)]
279    fn render_node_from_applier(
280        &self,
281        applier: &mut MemoryApplier,
282        node_id: NodeId,
283        parent_offset: Point,
284        operations: &mut Vec<RenderOp>,
285    ) {
286        // Read layout state and node data from LayoutNode
287        let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
288            let state = node.layout_state();
289            let modifier_slices = node.modifier_slices_snapshot();
290            let children: Vec<NodeId> = node.children.clone();
291            (state, modifier_slices, children)
292        }) {
293            Ok(data) => data,
294            Err(_) => return, // Node not found or type mismatch
295        };
296
297        let (layout_state, modifier_slices, children) = node_data;
298
299        // Skip nodes that weren't placed
300        if !layout_state.is_placed {
301            return;
302        }
303
304        // Calculate absolute position
305        let abs_x = parent_offset.x + layout_state.position.x;
306        let abs_y = parent_offset.y + layout_state.position.y;
307
308        let rect = Rect {
309            x: abs_x,
310            y: abs_y,
311            width: layout_state.size.width,
312            height: layout_state.size.height,
313        };
314
315        let size = Size {
316            width: rect.width,
317            height: rect.height,
318        };
319
320        // Collect draw commands from modifier slices
321        let mut behind = Vec::new();
322        let mut overlay = Vec::new();
323        behind.extend(collect_primitives_from_commands(
324            node_id,
325            rect,
326            size,
327            modifier_slices.draw_commands(),
328            PaintLayer::Behind,
329        ));
330        overlay.extend(collect_primitives_from_commands(
331            node_id,
332            rect,
333            size,
334            modifier_slices.draw_commands(),
335            PaintLayer::Overlay,
336        ));
337
338        operations.append(&mut behind);
339
340        // Render text content if present
341        if let Some(text) = modifier_slices.text_content() {
342            operations.push(RenderOp::Text {
343                node_id,
344                rect,
345                value: text.to_string(),
346            });
347        }
348
349        // Calculate content offset for children (includes node position + content_offset from padding etc.)
350        let child_offset = Point {
351            x: abs_x + layout_state.content_offset.x,
352            y: abs_y + layout_state.content_offset.y,
353        };
354
355        // Render children
356        for child_id in children {
357            self.render_node_from_applier(applier, child_id, child_offset, operations);
358        }
359
360        operations.append(&mut overlay);
361    }
362}
363
364#[cfg(test)]
365#[path = "tests/renderer_tests.rs"]
366mod tests;