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,
15 Content,
17 Overlay,
19}
20
21#[derive(Clone, Debug, PartialEq)]
23pub enum RenderOp {
24 Primitive {
26 node_id: NodeId,
28 layer: PaintLayer,
30 primitive: DrawPrimitive,
32 },
33 Text {
35 node_id: NodeId,
37 rect: Rect,
39 value: String,
41 },
42}
43
44#[derive(Clone, Debug, Default, PartialEq)]
46pub struct RecordedRenderScene {
47 operations: Vec<RenderOp>,
48}
49
50impl RecordedRenderScene {
51 pub fn new(operations: Vec<RenderOp>) -> Self {
53 Self { operations }
54 }
55
56 pub fn operations(&self) -> &[RenderOp] {
58 &self.operations
59 }
60
61 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#[derive(Default)]
76pub struct HeadlessRenderer;
77
78impl HeadlessRenderer {
79 pub fn new() -> Self {
81 Self
82 }
83
84 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
206fn 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 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 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;