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