Skip to main content

cranpose_ui/
renderer.rs

1use cranpose_core::{MemoryApplier, NodeId};
2use cranpose_ui_graphics::DrawPrimitive;
3
4use crate::{
5    layout::{LayoutBox, LayoutNodeData, LayoutTree},
6    modifier::{DrawCommand as ModifierDrawCommand, Point, Rect, Size},
7    widgets::LayoutNode,
8};
9
10/// Layer that a paint operation targets within the rendering pipeline.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum PaintLayer {
13    Behind,
14    Content,
15    Overlay,
16}
17
18/// A rendered operation emitted by the headless renderer.
19#[derive(Clone, Debug, PartialEq)]
20pub enum RenderOp {
21    Primitive {
22        node_id: NodeId,
23        layer: PaintLayer,
24        primitive: DrawPrimitive,
25    },
26    Text {
27        node_id: NodeId,
28        rect: Rect,
29        value: String,
30    },
31}
32
33/// A collection of render operations for a composed scene.
34#[derive(Clone, Debug, Default, PartialEq)]
35pub struct RecordedRenderScene {
36    operations: Vec<RenderOp>,
37}
38
39impl RecordedRenderScene {
40    pub fn new(operations: Vec<RenderOp>) -> Self {
41        Self { operations }
42    }
43
44    /// Returns a slice of recorded render operations in submission order.
45    pub fn operations(&self) -> &[RenderOp] {
46        &self.operations
47    }
48
49    /// Returns an iterator over primitives that target the provided paint layer.
50    pub fn primitives_for(&self, layer: PaintLayer) -> impl Iterator<Item = &DrawPrimitive> {
51        self.operations.iter().filter_map(move |op| match op {
52            RenderOp::Primitive {
53                layer: op_layer,
54                primitive,
55                ..
56            } if *op_layer == layer => Some(primitive),
57            _ => None,
58        })
59    }
60}
61
62/// A lightweight renderer that walks the layout tree and materialises paint commands.
63#[derive(Default)]
64pub struct HeadlessRenderer;
65
66impl HeadlessRenderer {
67    pub fn new() -> Self {
68        Self
69    }
70
71    pub fn render(&self, tree: &LayoutTree) -> RecordedRenderScene {
72        let mut operations = Vec::new();
73        self.render_box(tree.root(), &mut operations);
74        RecordedRenderScene::new(operations)
75    }
76
77    fn render_box(&self, layout: &LayoutBox, operations: &mut Vec<RenderOp>) {
78        let rect = layout.rect;
79        let (mut behind, overlay) = evaluate_modifier(layout.node_id, &layout.node_data, rect);
80
81        operations.append(&mut behind);
82
83        if let Some(text) = layout.node_data.modifier_slices().text_content() {
84            operations.push(RenderOp::Text {
85                node_id: layout.node_id,
86                rect,
87                value: text.to_string(),
88            });
89        }
90
91        for child in &layout.children {
92            self.render_box(child, operations);
93        }
94
95        append_overlay(operations, layout.node_id, rect, overlay);
96    }
97}
98
99enum PendingOverlay<'a> {
100    Recorded(std::vec::IntoIter<DrawPrimitive>),
101    Command(&'a crate::draw::DrawCommandFn),
102}
103
104fn evaluate_modifier(
105    node_id: NodeId,
106    data: &LayoutNodeData,
107    rect: Rect,
108) -> (Vec<RenderOp>, Vec<PendingOverlay<'_>>) {
109    let size = Size {
110        width: rect.width,
111        height: rect.height,
112    };
113
114    collect_primitives_from_commands(node_id, rect, size, data.modifier_slices().draw_commands())
115}
116
117fn collect_primitives_from_commands(
118    node_id: NodeId,
119    rect: Rect,
120    size: Size,
121    commands: &[ModifierDrawCommand],
122) -> (Vec<RenderOp>, Vec<PendingOverlay<'_>>) {
123    let mut behind = Vec::new();
124    let mut overlay = Vec::new();
125    for command in commands {
126        match command {
127            ModifierDrawCommand::Behind(func) => {
128                for primitive in record(func, size) {
129                    append_primitive(&mut behind, node_id, rect, PaintLayer::Behind, primitive);
130                }
131            }
132            ModifierDrawCommand::Overlay(func) => {
133                overlay.push(PendingOverlay::Command(func));
134            }
135            ModifierDrawCommand::WithContent(func) => {
136                let primitives = record(func, size);
137                let last_content = primitives
138                    .iter()
139                    .rposition(|primitive| matches!(primitive, DrawPrimitive::Content));
140                let mut primitives = primitives.into_iter();
141                if let Some(last_content) = last_content {
142                    for primitive in primitives.by_ref().take(last_content) {
143                        append_primitive(&mut behind, node_id, rect, PaintLayer::Behind, primitive);
144                    }
145                    let _ = primitives.next();
146                }
147                if !primitives.as_slice().is_empty() {
148                    overlay.push(PendingOverlay::Recorded(primitives));
149                }
150            }
151        }
152    }
153    (behind, overlay)
154}
155
156fn record(func: &crate::draw::DrawCommandFn, size: Size) -> Vec<DrawPrimitive> {
157    use cranpose_ui_graphics::DrawScope as _;
158    let mut scope = crate::draw::command_draw_scope(size);
159    func(&mut scope);
160    scope.into_primitives()
161}
162
163fn append_overlay(
164    operations: &mut Vec<RenderOp>,
165    node_id: NodeId,
166    rect: Rect,
167    pending: Vec<PendingOverlay<'_>>,
168) {
169    let size = Size {
170        width: rect.width,
171        height: rect.height,
172    };
173    for part in pending {
174        let primitives = match part {
175            PendingOverlay::Recorded(primitives) => primitives,
176            PendingOverlay::Command(func) => record(func, size).into_iter(),
177        };
178        for primitive in primitives {
179            append_primitive(operations, node_id, rect, PaintLayer::Overlay, primitive);
180        }
181    }
182}
183
184fn append_primitive(
185    operations: &mut Vec<RenderOp>,
186    node_id: NodeId,
187    rect: Rect,
188    layer: PaintLayer,
189    primitive: DrawPrimitive,
190) {
191    if !matches!(primitive, DrawPrimitive::Content) {
192        operations.push(RenderOp::Primitive {
193            node_id,
194            layer,
195            primitive: primitive.translate(rect.x, rect.y),
196        });
197    }
198}
199
200impl HeadlessRenderer {
201    /// Renders the scene by traversing LayoutNodes directly via the Applier.
202    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
203    pub fn render_from_applier(
204        &self,
205        applier: &mut MemoryApplier,
206        root: NodeId,
207    ) -> RecordedRenderScene {
208        let mut operations = Vec::new();
209        let mut child_stack = Vec::new();
210        self.render_node_from_applier(
211            applier,
212            root,
213            Point::default(),
214            &mut operations,
215            &mut child_stack,
216        );
217        RecordedRenderScene::new(operations)
218    }
219
220    /// Records `node_id` and its subtree. `child_stack` is shared by the whole
221    /// walk: a node pushes its children, visits them while its descendants
222    /// push and pop above them, and pops them, so no child list is copied out
223    /// of the applier.
224    fn render_node_from_applier(
225        &self,
226        applier: &mut MemoryApplier,
227        node_id: NodeId,
228        parent_offset: Point,
229        operations: &mut Vec<RenderOp>,
230        child_stack: &mut Vec<NodeId>,
231    ) {
232        let first_child = child_stack.len();
233        let Ok(Some((layout_state, modifier_slices))) =
234            applier.with_node::<LayoutNode, _>(node_id, |node| {
235                let state = node.layout_state();
236                state.is_placed().then(|| {
237                    child_stack.extend_from_slice(&node.children);
238                    (state, node.modifier_slices_snapshot())
239                })
240            })
241        else {
242            return;
243        };
244
245        let abs_x = parent_offset.x + layout_state.position().x;
246        let abs_y = parent_offset.y + layout_state.position().y;
247
248        let rect = Rect {
249            x: abs_x,
250            y: abs_y,
251            width: layout_state.size().width,
252            height: layout_state.size().height,
253        };
254
255        let size = Size {
256            width: rect.width,
257            height: rect.height,
258        };
259
260        let (behind, overlay) =
261            collect_primitives_from_commands(node_id, rect, size, modifier_slices.draw_commands());
262        operations.extend(behind);
263
264        if let Some(text) = modifier_slices.text_content() {
265            operations.push(RenderOp::Text {
266                node_id,
267                rect,
268                value: text.to_string(),
269            });
270        }
271
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        for index in first_child..child_stack.len() {
278            let child_id = child_stack[index];
279            self.render_node_from_applier(applier, child_id, child_offset, operations, child_stack);
280        }
281        child_stack.truncate(first_child);
282
283        append_overlay(operations, node_id, rect, overlay);
284    }
285}
286
287#[cfg(test)]
288#[path = "tests/renderer_tests.rs"]
289mod tests;