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    #[allow(clippy::only_used_in_recursion)]
78    fn render_box(&self, layout: &LayoutBox, operations: &mut Vec<RenderOp>) {
79        let rect = layout.rect;
80        let (mut behind, mut overlay) = evaluate_modifier(layout.node_id, &layout.node_data, rect);
81
82        operations.append(&mut behind);
83
84        if let Some(text) = layout.node_data.modifier_slices().text_content() {
85            operations.push(RenderOp::Text {
86                node_id: layout.node_id,
87                rect,
88                value: text.to_string(),
89            });
90        }
91
92        for child in &layout.children {
93            self.render_box(child, operations);
94        }
95
96        operations.append(&mut overlay);
97    }
98}
99
100fn evaluate_modifier(
101    node_id: NodeId,
102    data: &LayoutNodeData,
103    rect: Rect,
104) -> (Vec<RenderOp>, Vec<RenderOp>) {
105    let size = Size {
106        width: rect.width,
107        height: rect.height,
108    };
109
110    let behind = collect_primitives_from_commands(
111        node_id,
112        rect,
113        size,
114        data.modifier_slices().draw_commands(),
115        PaintLayer::Behind,
116    );
117    let overlay = collect_primitives_from_commands(
118        node_id,
119        rect,
120        size,
121        data.modifier_slices().draw_commands(),
122        PaintLayer::Overlay,
123    );
124    (behind, overlay)
125}
126
127fn collect_primitives_from_commands(
128    node_id: NodeId,
129    rect: Rect,
130    size: Size,
131    commands: &[ModifierDrawCommand],
132    layer: PaintLayer,
133) -> Vec<RenderOp> {
134    let split_with_content = |primitives: Vec<DrawPrimitive>, layer| {
135        let Some(last_content_idx) = primitives
136            .iter()
137            .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
138        else {
139            return if layer == PaintLayer::Overlay {
140                primitives
141                    .into_iter()
142                    .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
143                    .collect()
144            } else {
145                Vec::new()
146            };
147        };
148
149        primitives
150            .into_iter()
151            .enumerate()
152            .filter_map(|(index, primitive)| {
153                if matches!(primitive, DrawPrimitive::Content) {
154                    return None;
155                }
156                let is_before = index < last_content_idx;
157                match layer {
158                    PaintLayer::Behind if is_before => Some(primitive),
159                    PaintLayer::Overlay if !is_before => Some(primitive),
160                    _ => None,
161                }
162            })
163            .collect()
164    };
165
166    let run = |func: &crate::draw::DrawCommandFn| {
167        use cranpose_ui_graphics::DrawScope as _;
168        let mut scope = crate::draw::command_draw_scope(size);
169        func(&mut scope);
170        scope.into_primitives()
171    };
172    let mut ops = Vec::new();
173    for command in commands {
174        let primitives = match (layer, command) {
175            (PaintLayer::Behind, ModifierDrawCommand::Behind(func)) => run(func)
176                .into_iter()
177                .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
178                .collect(),
179            (PaintLayer::Overlay, ModifierDrawCommand::Overlay(func)) => run(func)
180                .into_iter()
181                .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
182                .collect(),
183            (PaintLayer::Behind | PaintLayer::Overlay, ModifierDrawCommand::WithContent(func)) => {
184                split_with_content(run(func), 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 {
210            rect,
211            brush,
212            stroke,
213        } => DrawPrimitive::Rect {
214            rect: rect.translate(dx, dy),
215            brush,
216            stroke,
217        },
218        DrawPrimitive::RoundRect {
219            rect,
220            brush,
221            radii,
222            stroke,
223        } => DrawPrimitive::RoundRect {
224            rect: rect.translate(dx, dy),
225            brush,
226            radii,
227            stroke,
228        },
229        DrawPrimitive::Arc {
230            rect,
231            brush,
232            center,
233            radius,
234            start_angle,
235            sweep_angle,
236            stroke,
237            inner_radius,
238        } => DrawPrimitive::Arc {
239            rect: rect.translate(dx, dy),
240            brush,
241            center: Point::new(center.x + dx, center.y + dy),
242            radius,
243            start_angle,
244            sweep_angle,
245            stroke,
246            inner_radius,
247        },
248        DrawPrimitive::Image {
249            rect,
250            image,
251            alpha,
252            color_filter,
253            sampling,
254            src_rect,
255        } => DrawPrimitive::Image {
256            rect: rect.translate(dx, dy),
257            image,
258            alpha,
259            color_filter,
260            sampling,
261            src_rect,
262        },
263        DrawPrimitive::Text(mut text) => {
264            text.rect = text.rect.translate(dx, dy);
265            DrawPrimitive::Text(text)
266        }
267        DrawPrimitive::Shadow(shadow) => {
268            use cranpose_ui_graphics::ShadowPrimitive;
269            DrawPrimitive::Shadow(match shadow {
270                ShadowPrimitive::Drop {
271                    shape,
272                    cutout,
273                    blur_radius,
274                    blend_mode,
275                } => ShadowPrimitive::Drop {
276                    shape: Box::new(translate_primitive(*shape, dx, dy)),
277                    cutout: cutout.map(|cutout| Box::new(translate_primitive(*cutout, dx, dy))),
278                    blur_radius,
279                    blend_mode,
280                },
281                ShadowPrimitive::Inner {
282                    fill,
283                    cutout,
284                    blur_radius,
285                    blend_mode,
286                    clip_rect,
287                } => ShadowPrimitive::Inner {
288                    fill: Box::new(translate_primitive(*fill, dx, dy)),
289                    cutout: Box::new(translate_primitive(*cutout, dx, dy)),
290                    blur_radius,
291                    blend_mode,
292                    clip_rect: clip_rect.translate(dx, dy),
293                },
294            })
295        }
296    }
297}
298
299impl HeadlessRenderer {
300    /// Renders the scene by traversing LayoutNodes directly via the Applier.
301    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
302    pub fn render_from_applier(
303        &self,
304        applier: &mut MemoryApplier,
305        root: NodeId,
306    ) -> RecordedRenderScene {
307        let mut operations = Vec::new();
308        self.render_node_from_applier(applier, root, Point::default(), &mut operations);
309        RecordedRenderScene::new(operations)
310    }
311
312    #[allow(clippy::only_used_in_recursion)]
313    fn render_node_from_applier(
314        &self,
315        applier: &mut MemoryApplier,
316        node_id: NodeId,
317        parent_offset: Point,
318        operations: &mut Vec<RenderOp>,
319    ) {
320        let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
321            let state = node.layout_state();
322            let modifier_slices = node.modifier_slices_snapshot();
323            let children: Vec<NodeId> = node.children.clone();
324            (state, modifier_slices, children)
325        }) {
326            Ok(data) => data,
327            Err(_) => return,
328        };
329
330        let (layout_state, modifier_slices, children) = node_data;
331
332        if !layout_state.is_placed() {
333            return;
334        }
335
336        let abs_x = parent_offset.x + layout_state.position().x;
337        let abs_y = parent_offset.y + layout_state.position().y;
338
339        let rect = Rect {
340            x: abs_x,
341            y: abs_y,
342            width: layout_state.size().width,
343            height: layout_state.size().height,
344        };
345
346        let size = Size {
347            width: rect.width,
348            height: rect.height,
349        };
350
351        let mut behind = Vec::new();
352        let mut overlay = Vec::new();
353        behind.extend(collect_primitives_from_commands(
354            node_id,
355            rect,
356            size,
357            modifier_slices.draw_commands(),
358            PaintLayer::Behind,
359        ));
360        overlay.extend(collect_primitives_from_commands(
361            node_id,
362            rect,
363            size,
364            modifier_slices.draw_commands(),
365            PaintLayer::Overlay,
366        ));
367
368        operations.append(&mut behind);
369
370        if let Some(text) = modifier_slices.text_content() {
371            operations.push(RenderOp::Text {
372                node_id,
373                rect,
374                value: text.to_string(),
375            });
376        }
377
378        let child_offset = Point {
379            x: abs_x + layout_state.content_offset.x,
380            y: abs_y + layout_state.content_offset.y,
381        };
382
383        for child_id in children {
384            self.render_node_from_applier(applier, child_id, child_offset, operations);
385        }
386
387        operations.append(&mut overlay);
388    }
389}
390
391#[cfg(test)]
392#[path = "tests/renderer_tests.rs"]
393mod tests;