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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum PaintLayer {
13 Behind,
14 Content,
15 Overlay,
16}
17
18#[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#[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 pub fn operations(&self) -> &[RenderOp] {
46 &self.operations
47 }
48
49 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#[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 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 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;