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, mut 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        operations.append(&mut overlay);
96    }
97}
98
99fn evaluate_modifier(
100    node_id: NodeId,
101    data: &LayoutNodeData,
102    rect: Rect,
103) -> (Vec<RenderOp>, Vec<RenderOp>) {
104    let size = Size {
105        width: rect.width,
106        height: rect.height,
107    };
108
109    let behind = collect_primitives_from_commands(
110        node_id,
111        rect,
112        size,
113        data.modifier_slices().draw_commands(),
114        PaintLayer::Behind,
115    );
116    let overlay = collect_primitives_from_commands(
117        node_id,
118        rect,
119        size,
120        data.modifier_slices().draw_commands(),
121        PaintLayer::Overlay,
122    );
123    (behind, overlay)
124}
125
126fn collect_primitives_from_commands(
127    node_id: NodeId,
128    rect: Rect,
129    size: Size,
130    commands: &[ModifierDrawCommand],
131    layer: PaintLayer,
132) -> Vec<RenderOp> {
133    let split_with_content = |primitives: Vec<DrawPrimitive>, layer| {
134        let Some(last_content_idx) = primitives
135            .iter()
136            .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
137        else {
138            return if layer == PaintLayer::Overlay {
139                primitives
140                    .into_iter()
141                    .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
142                    .collect()
143            } else {
144                Vec::new()
145            };
146        };
147
148        primitives
149            .into_iter()
150            .enumerate()
151            .filter_map(|(index, primitive)| {
152                if matches!(primitive, DrawPrimitive::Content) {
153                    return None;
154                }
155                let is_before = index < last_content_idx;
156                match layer {
157                    PaintLayer::Behind if is_before => Some(primitive),
158                    PaintLayer::Overlay if !is_before => Some(primitive),
159                    _ => None,
160                }
161            })
162            .collect()
163    };
164
165    let run = |func: &crate::draw::DrawCommandFn| {
166        use cranpose_ui_graphics::DrawScope as _;
167        let mut scope = crate::draw::command_draw_scope(size);
168        func(&mut scope);
169        scope.into_primitives()
170    };
171    let mut ops = Vec::new();
172    for command in commands {
173        let primitives = match (layer, command) {
174            (PaintLayer::Behind, ModifierDrawCommand::Behind(func)) => run(func)
175                .into_iter()
176                .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
177                .collect(),
178            (PaintLayer::Overlay, ModifierDrawCommand::Overlay(func)) => run(func)
179                .into_iter()
180                .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
181                .collect(),
182            (PaintLayer::Behind | PaintLayer::Overlay, ModifierDrawCommand::WithContent(func)) => {
183                split_with_content(run(func), layer)
184            }
185            _ => Vec::new(),
186        };
187        for primitive in primitives {
188            ops.push(RenderOp::Primitive {
189                node_id,
190                layer,
191                primitive: primitive.translate(rect.x, rect.y),
192            });
193        }
194    }
195    ops
196}
197
198impl HeadlessRenderer {
199    /// Renders the scene by traversing LayoutNodes directly via the Applier.
200    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
201    pub fn render_from_applier(
202        &self,
203        applier: &mut MemoryApplier,
204        root: NodeId,
205    ) -> RecordedRenderScene {
206        let mut operations = Vec::new();
207        let mut child_stack = Vec::new();
208        self.render_node_from_applier(
209            applier,
210            root,
211            Point::default(),
212            &mut operations,
213            &mut child_stack,
214        );
215        RecordedRenderScene::new(operations)
216    }
217
218    /// Records `node_id` and its subtree. `child_stack` is shared by the whole
219    /// walk: a node pushes its children, visits them while its descendants
220    /// push and pop above them, and pops them, so no child list is copied out
221    /// of the applier.
222    fn render_node_from_applier(
223        &self,
224        applier: &mut MemoryApplier,
225        node_id: NodeId,
226        parent_offset: Point,
227        operations: &mut Vec<RenderOp>,
228        child_stack: &mut Vec<NodeId>,
229    ) {
230        let first_child = child_stack.len();
231        let Ok(Some((layout_state, modifier_slices))) =
232            applier.with_node::<LayoutNode, _>(node_id, |node| {
233                let state = node.layout_state();
234                state.is_placed().then(|| {
235                    child_stack.extend_from_slice(&node.children);
236                    (state, node.modifier_slices_snapshot())
237                })
238            })
239        else {
240            return;
241        };
242
243        let abs_x = parent_offset.x + layout_state.position().x;
244        let abs_y = parent_offset.y + layout_state.position().y;
245
246        let rect = Rect {
247            x: abs_x,
248            y: abs_y,
249            width: layout_state.size().width,
250            height: layout_state.size().height,
251        };
252
253        let size = Size {
254            width: rect.width,
255            height: rect.height,
256        };
257
258        operations.extend(collect_primitives_from_commands(
259            node_id,
260            rect,
261            size,
262            modifier_slices.draw_commands(),
263            PaintLayer::Behind,
264        ));
265
266        if let Some(text) = modifier_slices.text_content() {
267            operations.push(RenderOp::Text {
268                node_id,
269                rect,
270                value: text.to_string(),
271            });
272        }
273
274        let child_offset = Point {
275            x: abs_x + layout_state.content_offset.x,
276            y: abs_y + layout_state.content_offset.y,
277        };
278
279        for index in first_child..child_stack.len() {
280            let child_id = child_stack[index];
281            self.render_node_from_applier(applier, child_id, child_offset, operations, child_stack);
282        }
283        child_stack.truncate(first_child);
284
285        operations.extend(collect_primitives_from_commands(
286            node_id,
287            rect,
288            size,
289            modifier_slices.draw_commands(),
290            PaintLayer::Overlay,
291        ));
292    }
293}
294
295#[cfg(test)]
296#[path = "tests/renderer_tests.rs"]
297mod tests;