cranpose_ui/
renderer.rs

1use crate::layout::{LayoutBox, LayoutNodeData, LayoutTree};
2use crate::modifier::{DrawCommand as ModifierDrawCommand, Rect, Size};
3use cranpose_core::NodeId;
4use cranpose_ui_graphics::DrawPrimitive;
5
6/// Layer that a paint operation targets within the rendering pipeline.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum PaintLayer {
9    Behind,
10    Content,
11    Overlay,
12}
13
14/// A rendered operation emitted by the headless renderer stub.
15#[derive(Clone, Debug, PartialEq)]
16pub enum RenderOp {
17    Primitive {
18        node_id: NodeId,
19        layer: PaintLayer,
20        primitive: DrawPrimitive,
21    },
22    Text {
23        node_id: NodeId,
24        rect: Rect,
25        value: String,
26    },
27}
28
29/// A collection of render operations for a composed scene.
30#[derive(Clone, Debug, Default, PartialEq)]
31pub struct RecordedRenderScene {
32    operations: Vec<RenderOp>,
33}
34
35impl RecordedRenderScene {
36    pub fn new(operations: Vec<RenderOp>) -> Self {
37        Self { operations }
38    }
39
40    /// Returns a slice of recorded render operations in submission order.
41    pub fn operations(&self) -> &[RenderOp] {
42        &self.operations
43    }
44
45    /// Consumes the scene and yields the owned operations.
46    pub fn into_operations(self) -> Vec<RenderOp> {
47        self.operations
48    }
49
50    /// Returns an iterator over primitives that target the provided paint layer.
51    pub fn primitives_for(&self, layer: PaintLayer) -> impl Iterator<Item = &DrawPrimitive> {
52        self.operations.iter().filter_map(move |op| match op {
53            RenderOp::Primitive {
54                layer: op_layer,
55                primitive,
56                ..
57            } if *op_layer == layer => Some(primitive),
58            _ => None,
59        })
60    }
61}
62
63/// A lightweight renderer that walks the layout tree and materialises paint commands.
64#[derive(Default)]
65pub struct HeadlessRenderer;
66
67impl HeadlessRenderer {
68    pub fn new() -> Self {
69        Self
70    }
71
72    pub fn render(&self, tree: &LayoutTree) -> RecordedRenderScene {
73        let mut operations = Vec::new();
74        self.render_box(tree.root(), &mut operations);
75        RecordedRenderScene::new(operations)
76    }
77
78    #[allow(clippy::only_used_in_recursion)]
79    fn render_box(&self, layout: &LayoutBox, operations: &mut Vec<RenderOp>) {
80        let rect = layout.rect;
81        let (mut behind, mut overlay) = evaluate_modifier(layout.node_id, &layout.node_data, rect);
82
83        operations.append(&mut behind);
84
85        // Render text content if present in modifier slices.
86        // This follows Jetpack Compose's pattern where text is a modifier node capability
87        // (TextModifierNode implements LayoutModifierNode + DrawModifierNode + SemanticsNode)
88        if let Some(text) = layout.node_data.modifier_slices().text_content() {
89            operations.push(RenderOp::Text {
90                node_id: layout.node_id,
91                rect,
92                value: text.to_string(),
93            });
94        }
95
96        // Render children
97        for child in &layout.children {
98            self.render_box(child, operations);
99        }
100
101        operations.append(&mut overlay);
102    }
103}
104
105fn evaluate_modifier(
106    node_id: NodeId,
107    data: &LayoutNodeData,
108    rect: Rect,
109) -> (Vec<RenderOp>, Vec<RenderOp>) {
110    let mut behind = Vec::new();
111    let mut overlay = Vec::new();
112
113    let size = Size {
114        width: rect.width,
115        height: rect.height,
116    };
117
118    // Render via modifier slices - all drawing now goes through draw commands
119    // collected from the modifier node chain, including backgrounds, borders, etc.
120    for command in data.modifier_slices().draw_commands() {
121        match command {
122            ModifierDrawCommand::Behind(func) => {
123                for primitive in func(size) {
124                    behind.push(RenderOp::Primitive {
125                        node_id,
126                        layer: PaintLayer::Behind,
127                        primitive: translate_primitive(primitive, rect.x, rect.y),
128                    });
129                }
130            }
131            ModifierDrawCommand::Overlay(func) => {
132                for primitive in func(size) {
133                    overlay.push(RenderOp::Primitive {
134                        node_id,
135                        layer: PaintLayer::Overlay,
136                        primitive: translate_primitive(primitive, rect.x, rect.y),
137                    });
138                }
139            }
140        }
141    }
142
143    (behind, overlay)
144}
145
146fn translate_primitive(primitive: DrawPrimitive, dx: f32, dy: f32) -> DrawPrimitive {
147    match primitive {
148        DrawPrimitive::Rect { rect, brush } => DrawPrimitive::Rect {
149            rect: rect.translate(dx, dy),
150            brush,
151        },
152        DrawPrimitive::RoundRect { rect, brush, radii } => DrawPrimitive::RoundRect {
153            rect: rect.translate(dx, dy),
154            brush,
155            radii,
156        },
157    }
158}
159
160#[cfg(test)]
161#[path = "tests/renderer_tests.rs"]
162mod tests;