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: translate_primitive(primitive, rect.x, rect.y),
192            });
193        }
194    }
195    ops
196}
197
198fn translate_primitive(primitive: DrawPrimitive, dx: f32, dy: f32) -> DrawPrimitive {
199    match primitive {
200        DrawPrimitive::Content => DrawPrimitive::Content,
201        DrawPrimitive::Blend {
202            primitive,
203            blend_mode,
204        } => DrawPrimitive::Blend {
205            primitive: Box::new(translate_primitive(*primitive, dx, dy)),
206            blend_mode,
207        },
208        DrawPrimitive::Rect {
209            rect,
210            brush,
211            stroke,
212        } => DrawPrimitive::Rect {
213            rect: rect.translate(dx, dy),
214            brush,
215            stroke,
216        },
217        DrawPrimitive::RoundRect {
218            rect,
219            brush,
220            radii,
221            stroke,
222        } => DrawPrimitive::RoundRect {
223            rect: rect.translate(dx, dy),
224            brush,
225            radii,
226            stroke,
227        },
228        DrawPrimitive::Arc {
229            rect,
230            brush,
231            center,
232            radius,
233            start_angle,
234            sweep_angle,
235            stroke,
236            inner_radius,
237        } => DrawPrimitive::Arc {
238            rect: rect.translate(dx, dy),
239            brush,
240            center: Point::new(center.x + dx, center.y + dy),
241            radius,
242            start_angle,
243            sweep_angle,
244            stroke,
245            inner_radius,
246        },
247        DrawPrimitive::Image {
248            rect,
249            image,
250            alpha,
251            color_filter,
252            sampling,
253            src_rect,
254        } => DrawPrimitive::Image {
255            rect: rect.translate(dx, dy),
256            image,
257            alpha,
258            color_filter,
259            sampling,
260            src_rect,
261        },
262        DrawPrimitive::Text(mut text) => {
263            text.rect = text.rect.translate(dx, dy);
264            DrawPrimitive::Text(text)
265        }
266        DrawPrimitive::Shadow(shadow) => {
267            use cranpose_ui_graphics::ShadowPrimitive;
268            DrawPrimitive::Shadow(match shadow {
269                ShadowPrimitive::Drop {
270                    shape,
271                    cutout,
272                    blur_radius,
273                    blend_mode,
274                } => ShadowPrimitive::Drop {
275                    shape: Box::new(translate_primitive(*shape, dx, dy)),
276                    cutout: cutout.map(|cutout| Box::new(translate_primitive(*cutout, dx, dy))),
277                    blur_radius,
278                    blend_mode,
279                },
280                ShadowPrimitive::Inner {
281                    fill,
282                    cutout,
283                    blur_radius,
284                    blend_mode,
285                    clip_rect,
286                } => ShadowPrimitive::Inner {
287                    fill: Box::new(translate_primitive(*fill, dx, dy)),
288                    cutout: Box::new(translate_primitive(*cutout, dx, dy)),
289                    blur_radius,
290                    blend_mode,
291                    clip_rect: clip_rect.translate(dx, dy),
292                },
293            })
294        }
295    }
296}
297
298impl HeadlessRenderer {
299    /// Renders the scene by traversing LayoutNodes directly via the Applier.
300    /// This is the new architecture that eliminates per-frame LayoutTree reconstruction.
301    pub fn render_from_applier(
302        &self,
303        applier: &mut MemoryApplier,
304        root: NodeId,
305    ) -> RecordedRenderScene {
306        let mut operations = Vec::new();
307        self.render_node_from_applier(applier, root, Point::default(), &mut operations);
308        RecordedRenderScene::new(operations)
309    }
310
311    fn render_node_from_applier(
312        &self,
313        applier: &mut MemoryApplier,
314        node_id: NodeId,
315        parent_offset: Point,
316        operations: &mut Vec<RenderOp>,
317    ) {
318        let Ok(node_data) = applier.with_node::<LayoutNode, _>(node_id, |node| {
319            let state = node.layout_state();
320            let modifier_slices = node.modifier_slices_snapshot();
321            let children: Vec<NodeId> = node.children.clone();
322            (state, modifier_slices, children)
323        }) else {
324            return;
325        };
326
327        let (layout_state, modifier_slices, children) = node_data;
328
329        if !layout_state.is_placed() {
330            return;
331        }
332
333        let abs_x = parent_offset.x + layout_state.position().x;
334        let abs_y = parent_offset.y + layout_state.position().y;
335
336        let rect = Rect {
337            x: abs_x,
338            y: abs_y,
339            width: layout_state.size().width,
340            height: layout_state.size().height,
341        };
342
343        let size = Size {
344            width: rect.width,
345            height: rect.height,
346        };
347
348        let mut behind = Vec::new();
349        let mut overlay = Vec::new();
350        behind.extend(collect_primitives_from_commands(
351            node_id,
352            rect,
353            size,
354            modifier_slices.draw_commands(),
355            PaintLayer::Behind,
356        ));
357        overlay.extend(collect_primitives_from_commands(
358            node_id,
359            rect,
360            size,
361            modifier_slices.draw_commands(),
362            PaintLayer::Overlay,
363        ));
364
365        operations.append(&mut behind);
366
367        if let Some(text) = modifier_slices.text_content() {
368            operations.push(RenderOp::Text {
369                node_id,
370                rect,
371                value: text.to_string(),
372            });
373        }
374
375        let child_offset = Point {
376            x: abs_x + layout_state.content_offset.x,
377            y: abs_y + layout_state.content_offset.y,
378        };
379
380        for child_id in children {
381            self.render_node_from_applier(applier, child_id, child_offset, operations);
382        }
383
384        operations.append(&mut overlay);
385    }
386}
387
388#[cfg(test)]
389#[path = "tests/renderer_tests.rs"]
390mod tests;