1use crate::layout::{LayoutBox, LayoutNodeData, LayoutTree};
2use crate::modifier::{DrawCommand as ModifierDrawCommand, Point, Rect, Size};
3use crate::widgets::LayoutNode;
4use cranpose_core::{MemoryApplier, NodeId};
5use cranpose_ui_graphics::DrawPrimitive;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum PaintLayer {
10 Behind,
11 Content,
12 Overlay,
13}
14
15#[derive(Clone, Debug, PartialEq)]
17pub enum RenderOp {
18 Primitive {
19 node_id: NodeId,
20 layer: PaintLayer,
21 primitive: DrawPrimitive,
22 },
23 Text {
24 node_id: NodeId,
25 rect: Rect,
26 value: String,
27 },
28}
29
30#[derive(Clone, Debug, Default, PartialEq)]
32pub struct RecordedRenderScene {
33 operations: Vec<RenderOp>,
34}
35
36impl RecordedRenderScene {
37 pub fn new(operations: Vec<RenderOp>) -> Self {
38 Self { operations }
39 }
40
41 pub fn operations(&self) -> &[RenderOp] {
43 &self.operations
44 }
45
46 pub fn primitives_for(&self, layer: PaintLayer) -> impl Iterator<Item = &DrawPrimitive> {
48 self.operations.iter().filter_map(move |op| match op {
49 RenderOp::Primitive {
50 layer: op_layer,
51 primitive,
52 ..
53 } if *op_layer == layer => Some(primitive),
54 _ => None,
55 })
56 }
57}
58
59#[derive(Default)]
61pub struct HeadlessRenderer;
62
63impl HeadlessRenderer {
64 pub fn new() -> Self {
65 Self
66 }
67
68 pub fn render(&self, tree: &LayoutTree) -> RecordedRenderScene {
69 let mut operations = Vec::new();
70 self.render_box(tree.root(), &mut operations);
71 RecordedRenderScene::new(operations)
72 }
73
74 #[allow(clippy::only_used_in_recursion)]
75 fn render_box(&self, layout: &LayoutBox, operations: &mut Vec<RenderOp>) {
76 let rect = layout.rect;
77 let (mut behind, mut overlay) = evaluate_modifier(layout.node_id, &layout.node_data, rect);
78
79 operations.append(&mut behind);
80
81 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 {
94 self.render_box(child, operations);
95 }
96
97 operations.append(&mut overlay);
98 }
99}
100
101fn evaluate_modifier(
102 node_id: NodeId,
103 data: &LayoutNodeData,
104 rect: Rect,
105) -> (Vec<RenderOp>, Vec<RenderOp>) {
106 let size = Size {
107 width: rect.width,
108 height: rect.height,
109 };
110
111 let behind = collect_primitives_from_commands(
112 node_id,
113 rect,
114 size,
115 data.modifier_slices().draw_commands(),
116 PaintLayer::Behind,
117 );
118 let overlay = collect_primitives_from_commands(
119 node_id,
120 rect,
121 size,
122 data.modifier_slices().draw_commands(),
123 PaintLayer::Overlay,
124 );
125 (behind, overlay)
126}
127
128fn collect_primitives_from_commands(
129 node_id: NodeId,
130 rect: Rect,
131 size: Size,
132 commands: &[ModifierDrawCommand],
133 layer: PaintLayer,
134) -> Vec<RenderOp> {
135 let split_with_content = |primitives: Vec<DrawPrimitive>, layer| {
136 let Some(last_content_idx) = primitives
137 .iter()
138 .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
139 else {
140 return if layer == PaintLayer::Overlay {
141 primitives
142 .into_iter()
143 .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
144 .collect()
145 } else {
146 Vec::new()
147 };
148 };
149
150 primitives
151 .into_iter()
152 .enumerate()
153 .filter_map(|(index, primitive)| {
154 if matches!(primitive, DrawPrimitive::Content) {
155 return None;
156 }
157 let is_before = index < last_content_idx;
158 match layer {
159 PaintLayer::Behind if is_before => Some(primitive),
160 PaintLayer::Overlay if !is_before => Some(primitive),
161 _ => None,
162 }
163 })
164 .collect()
165 };
166
167 let run = |func: &crate::draw::DrawCommandFn| {
168 use cranpose_ui_graphics::DrawScope as _;
169 let mut scope = crate::draw::command_draw_scope(size);
170 func(&mut scope);
171 scope.into_primitives()
172 };
173 let mut ops = Vec::new();
174 for command in commands {
175 let primitives = match (layer, command) {
176 (PaintLayer::Behind, ModifierDrawCommand::Behind(func)) => run(func)
177 .into_iter()
178 .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
179 .collect(),
180 (PaintLayer::Overlay, ModifierDrawCommand::Overlay(func)) => run(func)
181 .into_iter()
182 .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
183 .collect(),
184 (PaintLayer::Behind | PaintLayer::Overlay, ModifierDrawCommand::WithContent(func)) => {
185 split_with_content(run(func), layer)
186 }
187 _ => Vec::new(),
188 };
189 for primitive in primitives {
190 ops.push(RenderOp::Primitive {
191 node_id,
192 layer,
193 primitive: translate_primitive(primitive, rect.x, rect.y),
194 });
195 }
196 }
197 ops
198}
199
200fn translate_primitive(primitive: DrawPrimitive, dx: f32, dy: f32) -> DrawPrimitive {
201 match primitive {
202 DrawPrimitive::Content => DrawPrimitive::Content,
203 DrawPrimitive::Blend {
204 primitive,
205 blend_mode,
206 } => DrawPrimitive::Blend {
207 primitive: Box::new(translate_primitive(*primitive, dx, dy)),
208 blend_mode,
209 },
210 DrawPrimitive::Rect {
211 rect,
212 brush,
213 stroke,
214 } => DrawPrimitive::Rect {
215 rect: rect.translate(dx, dy),
216 brush,
217 stroke,
218 },
219 DrawPrimitive::RoundRect {
220 rect,
221 brush,
222 radii,
223 stroke,
224 } => DrawPrimitive::RoundRect {
225 rect: rect.translate(dx, dy),
226 brush,
227 radii,
228 stroke,
229 },
230 DrawPrimitive::Arc {
231 rect,
232 brush,
233 center,
234 radius,
235 start_angle,
236 sweep_angle,
237 stroke,
238 inner_radius,
239 } => DrawPrimitive::Arc {
240 rect: rect.translate(dx, dy),
241 brush,
242 center: Point::new(center.x + dx, center.y + dy),
246 radius,
247 start_angle,
248 sweep_angle,
249 stroke,
250 inner_radius,
251 },
252 DrawPrimitive::Image {
253 rect,
254 image,
255 alpha,
256 color_filter,
257 sampling,
258 src_rect,
259 } => DrawPrimitive::Image {
260 rect: rect.translate(dx, dy),
261 image,
262 alpha,
263 color_filter,
264 sampling,
265 src_rect,
266 },
267 DrawPrimitive::Text(mut text) => {
268 text.rect = text.rect.translate(dx, dy);
269 DrawPrimitive::Text(text)
270 }
271 DrawPrimitive::Shadow(shadow) => {
272 use cranpose_ui_graphics::ShadowPrimitive;
273 DrawPrimitive::Shadow(match shadow {
274 ShadowPrimitive::Drop {
275 shape,
276 cutout,
277 blur_radius,
278 blend_mode,
279 } => ShadowPrimitive::Drop {
280 shape: Box::new(translate_primitive(*shape, dx, dy)),
281 cutout: cutout.map(|cutout| Box::new(translate_primitive(*cutout, dx, dy))),
282 blur_radius,
283 blend_mode,
284 },
285 ShadowPrimitive::Inner {
286 fill,
287 cutout,
288 blur_radius,
289 blend_mode,
290 clip_rect,
291 } => ShadowPrimitive::Inner {
292 fill: Box::new(translate_primitive(*fill, dx, dy)),
293 cutout: Box::new(translate_primitive(*cutout, dx, dy)),
294 blur_radius,
295 blend_mode,
296 clip_rect: clip_rect.translate(dx, dy),
297 },
298 })
299 }
300 }
301}
302
303impl HeadlessRenderer {
308 pub fn render_from_applier(
311 &self,
312 applier: &mut MemoryApplier,
313 root: NodeId,
314 ) -> RecordedRenderScene {
315 let mut operations = Vec::new();
316 self.render_node_from_applier(applier, root, Point::default(), &mut operations);
317 RecordedRenderScene::new(operations)
318 }
319
320 #[allow(clippy::only_used_in_recursion)]
321 fn render_node_from_applier(
322 &self,
323 applier: &mut MemoryApplier,
324 node_id: NodeId,
325 parent_offset: Point,
326 operations: &mut Vec<RenderOp>,
327 ) {
328 let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
330 let state = node.layout_state();
331 let modifier_slices = node.modifier_slices_snapshot();
332 let children: Vec<NodeId> = node.children.clone();
333 (state, modifier_slices, children)
334 }) {
335 Ok(data) => data,
336 Err(_) => return, };
338
339 let (layout_state, modifier_slices, children) = node_data;
340
341 if !layout_state.is_placed {
343 return;
344 }
345
346 let abs_x = parent_offset.x + layout_state.position.x;
348 let abs_y = parent_offset.y + layout_state.position.y;
349
350 let rect = Rect {
351 x: abs_x,
352 y: abs_y,
353 width: layout_state.size.width,
354 height: layout_state.size.height,
355 };
356
357 let size = Size {
358 width: rect.width,
359 height: rect.height,
360 };
361
362 let mut behind = Vec::new();
364 let mut overlay = Vec::new();
365 behind.extend(collect_primitives_from_commands(
366 node_id,
367 rect,
368 size,
369 modifier_slices.draw_commands(),
370 PaintLayer::Behind,
371 ));
372 overlay.extend(collect_primitives_from_commands(
373 node_id,
374 rect,
375 size,
376 modifier_slices.draw_commands(),
377 PaintLayer::Overlay,
378 ));
379
380 operations.append(&mut behind);
381
382 if let Some(text) = modifier_slices.text_content() {
384 operations.push(RenderOp::Text {
385 node_id,
386 rect,
387 value: text.to_string(),
388 });
389 }
390
391 let child_offset = Point {
393 x: abs_x + layout_state.content_offset.x,
394 y: abs_y + layout_state.content_offset.y,
395 };
396
397 for child_id in children {
399 self.render_node_from_applier(applier, child_id, child_offset, operations);
400 }
401
402 operations.append(&mut overlay);
403 }
404}
405
406#[cfg(test)]
407#[path = "tests/renderer_tests.rs"]
408mod tests;