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 for primitive in record(func, size) {
144 append_primitive(&mut behind, node_id, rect, PaintLayer::Behind, primitive);
145 }
146 }
147 ModifierDrawCommand::Overlay(func) => {
148 overlay.push(PendingOverlay::Command(func));
149 }
150 ModifierDrawCommand::WithContent(func) => {
151 let primitives = record(func, size);
152 let last_content = primitives
153 .iter()
154 .rposition(|primitive| matches!(primitive, DrawPrimitive::Content));
155 let mut primitives = primitives.into_iter();
156 if let Some(last_content) = last_content {
157 for primitive in primitives.by_ref().take(last_content) {
158 append_primitive(&mut behind, node_id, rect, PaintLayer::Behind, primitive);
159 }
160 let _ = primitives.next();
161 }
162 if !primitives.as_slice().is_empty() {
163 overlay.push(PendingOverlay::Recorded(primitives));
164 }
165 }
166 }
167 }
168 (behind, overlay)
169}
170
171fn record(func: &crate::draw::DrawCommandFn, size: Size) -> Vec<DrawPrimitive> {
172 use cranpose_ui_graphics::DrawScope as _;
173 let mut scope = crate::draw::command_draw_scope(size);
174 func(&mut scope);
175 scope.into_primitives()
176}
177
178fn append_overlay(
179 operations: &mut Vec<RenderOp>,
180 node_id: NodeId,
181 rect: Rect,
182 pending: Vec<PendingOverlay<'_>>,
183) {
184 let size = Size {
185 width: rect.width,
186 height: rect.height,
187 };
188 for part in pending {
189 let primitives = match part {
190 PendingOverlay::Recorded(primitives) => primitives,
191 PendingOverlay::Command(func) => record(func, size).into_iter(),
192 };
193 for primitive in primitives {
194 append_primitive(operations, node_id, rect, PaintLayer::Overlay, primitive);
195 }
196 }
197}
198
199fn append_primitive(
200 operations: &mut Vec<RenderOp>,
201 node_id: NodeId,
202 rect: Rect,
203 layer: PaintLayer,
204 primitive: DrawPrimitive,
205) {
206 if !matches!(primitive, DrawPrimitive::Content) {
207 operations.push(RenderOp::Primitive {
208 node_id,
209 layer,
210 primitive: primitive.translate(rect.x, rect.y),
211 });
212 }
213}
214
215impl HeadlessRenderer {
216 pub fn render_from_applier(
219 &self,
220 applier: &mut MemoryApplier,
221 root: NodeId,
222 ) -> RecordedRenderScene {
223 let mut operations = Vec::new();
224 let mut child_stack = Vec::new();
225 self.render_node_from_applier(
226 applier,
227 root,
228 Point::default(),
229 &mut operations,
230 &mut child_stack,
231 );
232 RecordedRenderScene::new(operations)
233 }
234
235 fn render_node_from_applier(
240 &self,
241 applier: &mut MemoryApplier,
242 node_id: NodeId,
243 parent_offset: Point,
244 operations: &mut Vec<RenderOp>,
245 child_stack: &mut Vec<NodeId>,
246 ) {
247 let first_child = child_stack.len();
248 let Ok(Some((layout_state, modifier_slices))) =
249 applier.with_node::<LayoutNode, _>(node_id, |node| {
250 let state = node.layout_state();
251 state.is_placed().then(|| {
252 child_stack.extend_from_slice(&node.children);
253 (state, node.modifier_slices_snapshot())
254 })
255 })
256 else {
257 return;
258 };
259
260 let abs_x = parent_offset.x + layout_state.position().x;
261 let abs_y = parent_offset.y + layout_state.position().y;
262
263 let rect = Rect {
264 x: abs_x,
265 y: abs_y,
266 width: layout_state.size().width,
267 height: layout_state.size().height,
268 };
269
270 let size = Size {
271 width: rect.width,
272 height: rect.height,
273 };
274
275 let (behind, overlay) =
276 collect_primitives_from_commands(node_id, rect, size, modifier_slices.draw_commands());
277 operations.extend(behind);
278
279 if let Some(text) = modifier_slices.text_content() {
280 operations.push(RenderOp::Text {
281 node_id,
282 rect,
283 value: text.to_string(),
284 });
285 }
286
287 let child_offset = Point {
288 x: abs_x + layout_state.content_offset().x,
289 y: abs_y + layout_state.content_offset().y,
290 };
291
292 for index in first_child..child_stack.len() {
293 let child_id = child_stack[index];
294 self.render_node_from_applier(applier, child_id, child_offset, operations, child_stack);
295 }
296 child_stack.truncate(first_child);
297
298 append_overlay(operations, node_id, rect, overlay);
299 }
300}
301
302#[cfg(test)]
303#[path = "tests/renderer_tests.rs"]
304mod tests;