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 #[allow(clippy::only_used_in_recursion)]
78 fn render_box(&self, layout: &LayoutBox, operations: &mut Vec<RenderOp>) {
79 let rect = layout.rect;
80 let (mut behind, mut overlay) = evaluate_modifier(layout.node_id, &layout.node_data, rect);
81
82 operations.append(&mut behind);
83
84 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 {
93 self.render_box(child, operations);
94 }
95
96 operations.append(&mut overlay);
97 }
98}
99
100fn evaluate_modifier(
101 node_id: NodeId,
102 data: &LayoutNodeData,
103 rect: Rect,
104) -> (Vec<RenderOp>, Vec<RenderOp>) {
105 let size = Size {
106 width: rect.width,
107 height: rect.height,
108 };
109
110 let behind = collect_primitives_from_commands(
111 node_id,
112 rect,
113 size,
114 data.modifier_slices().draw_commands(),
115 PaintLayer::Behind,
116 );
117 let overlay = collect_primitives_from_commands(
118 node_id,
119 rect,
120 size,
121 data.modifier_slices().draw_commands(),
122 PaintLayer::Overlay,
123 );
124 (behind, overlay)
125}
126
127fn collect_primitives_from_commands(
128 node_id: NodeId,
129 rect: Rect,
130 size: Size,
131 commands: &[ModifierDrawCommand],
132 layer: PaintLayer,
133) -> Vec<RenderOp> {
134 let split_with_content = |primitives: Vec<DrawPrimitive>, layer| {
135 let Some(last_content_idx) = primitives
136 .iter()
137 .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
138 else {
139 return if layer == PaintLayer::Overlay {
140 primitives
141 .into_iter()
142 .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
143 .collect()
144 } else {
145 Vec::new()
146 };
147 };
148
149 primitives
150 .into_iter()
151 .enumerate()
152 .filter_map(|(index, primitive)| {
153 if matches!(primitive, DrawPrimitive::Content) {
154 return None;
155 }
156 let is_before = index < last_content_idx;
157 match layer {
158 PaintLayer::Behind if is_before => Some(primitive),
159 PaintLayer::Overlay if !is_before => Some(primitive),
160 _ => None,
161 }
162 })
163 .collect()
164 };
165
166 let run = |func: &crate::draw::DrawCommandFn| {
167 use cranpose_ui_graphics::DrawScope as _;
168 let mut scope = crate::draw::command_draw_scope(size);
169 func(&mut scope);
170 scope.into_primitives()
171 };
172 let mut ops = Vec::new();
173 for command in commands {
174 let primitives = match (layer, command) {
175 (PaintLayer::Behind, ModifierDrawCommand::Behind(func)) => run(func)
176 .into_iter()
177 .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
178 .collect(),
179 (PaintLayer::Overlay, ModifierDrawCommand::Overlay(func)) => run(func)
180 .into_iter()
181 .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
182 .collect(),
183 (PaintLayer::Behind | PaintLayer::Overlay, ModifierDrawCommand::WithContent(func)) => {
184 split_with_content(run(func), layer)
185 }
186 _ => Vec::new(),
187 };
188 for primitive in primitives {
189 ops.push(RenderOp::Primitive {
190 node_id,
191 layer,
192 primitive: translate_primitive(primitive, rect.x, rect.y),
193 });
194 }
195 }
196 ops
197}
198
199fn translate_primitive(primitive: DrawPrimitive, dx: f32, dy: f32) -> DrawPrimitive {
200 match primitive {
201 DrawPrimitive::Content => DrawPrimitive::Content,
202 DrawPrimitive::Blend {
203 primitive,
204 blend_mode,
205 } => DrawPrimitive::Blend {
206 primitive: Box::new(translate_primitive(*primitive, dx, dy)),
207 blend_mode,
208 },
209 DrawPrimitive::Rect {
210 rect,
211 brush,
212 stroke,
213 } => DrawPrimitive::Rect {
214 rect: rect.translate(dx, dy),
215 brush,
216 stroke,
217 },
218 DrawPrimitive::RoundRect {
219 rect,
220 brush,
221 radii,
222 stroke,
223 } => DrawPrimitive::RoundRect {
224 rect: rect.translate(dx, dy),
225 brush,
226 radii,
227 stroke,
228 },
229 DrawPrimitive::Arc {
230 rect,
231 brush,
232 center,
233 radius,
234 start_angle,
235 sweep_angle,
236 stroke,
237 inner_radius,
238 } => DrawPrimitive::Arc {
239 rect: rect.translate(dx, dy),
240 brush,
241 center: Point::new(center.x + dx, center.y + dy),
242 radius,
243 start_angle,
244 sweep_angle,
245 stroke,
246 inner_radius,
247 },
248 DrawPrimitive::Image {
249 rect,
250 image,
251 alpha,
252 color_filter,
253 sampling,
254 src_rect,
255 } => DrawPrimitive::Image {
256 rect: rect.translate(dx, dy),
257 image,
258 alpha,
259 color_filter,
260 sampling,
261 src_rect,
262 },
263 DrawPrimitive::Text(mut text) => {
264 text.rect = text.rect.translate(dx, dy);
265 DrawPrimitive::Text(text)
266 }
267 DrawPrimitive::Shadow(shadow) => {
268 use cranpose_ui_graphics::ShadowPrimitive;
269 DrawPrimitive::Shadow(match shadow {
270 ShadowPrimitive::Drop {
271 shape,
272 cutout,
273 blur_radius,
274 blend_mode,
275 } => ShadowPrimitive::Drop {
276 shape: Box::new(translate_primitive(*shape, dx, dy)),
277 cutout: cutout.map(|cutout| Box::new(translate_primitive(*cutout, dx, dy))),
278 blur_radius,
279 blend_mode,
280 },
281 ShadowPrimitive::Inner {
282 fill,
283 cutout,
284 blur_radius,
285 blend_mode,
286 clip_rect,
287 } => ShadowPrimitive::Inner {
288 fill: Box::new(translate_primitive(*fill, dx, dy)),
289 cutout: Box::new(translate_primitive(*cutout, dx, dy)),
290 blur_radius,
291 blend_mode,
292 clip_rect: clip_rect.translate(dx, dy),
293 },
294 })
295 }
296 }
297}
298
299impl HeadlessRenderer {
300 pub fn render_from_applier(
303 &self,
304 applier: &mut MemoryApplier,
305 root: NodeId,
306 ) -> RecordedRenderScene {
307 let mut operations = Vec::new();
308 self.render_node_from_applier(applier, root, Point::default(), &mut operations);
309 RecordedRenderScene::new(operations)
310 }
311
312 #[allow(clippy::only_used_in_recursion)]
313 fn render_node_from_applier(
314 &self,
315 applier: &mut MemoryApplier,
316 node_id: NodeId,
317 parent_offset: Point,
318 operations: &mut Vec<RenderOp>,
319 ) {
320 let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
321 let state = node.layout_state();
322 let modifier_slices = node.modifier_slices_snapshot();
323 let children: Vec<NodeId> = node.children.clone();
324 (state, modifier_slices, children)
325 }) {
326 Ok(data) => data,
327 Err(_) => return,
328 };
329
330 let (layout_state, modifier_slices, children) = node_data;
331
332 if !layout_state.is_placed() {
333 return;
334 }
335
336 let abs_x = parent_offset.x + layout_state.position().x;
337 let abs_y = parent_offset.y + layout_state.position().y;
338
339 let rect = Rect {
340 x: abs_x,
341 y: abs_y,
342 width: layout_state.size().width,
343 height: layout_state.size().height,
344 };
345
346 let size = Size {
347 width: rect.width,
348 height: rect.height,
349 };
350
351 let mut behind = Vec::new();
352 let mut overlay = Vec::new();
353 behind.extend(collect_primitives_from_commands(
354 node_id,
355 rect,
356 size,
357 modifier_slices.draw_commands(),
358 PaintLayer::Behind,
359 ));
360 overlay.extend(collect_primitives_from_commands(
361 node_id,
362 rect,
363 size,
364 modifier_slices.draw_commands(),
365 PaintLayer::Overlay,
366 ));
367
368 operations.append(&mut behind);
369
370 if let Some(text) = modifier_slices.text_content() {
371 operations.push(RenderOp::Text {
372 node_id,
373 rect,
374 value: text.to_string(),
375 });
376 }
377
378 let child_offset = Point {
379 x: abs_x + layout_state.content_offset.x,
380 y: abs_y + layout_state.content_offset.y,
381 };
382
383 for child_id in children {
384 self.render_node_from_applier(applier, child_id, child_offset, operations);
385 }
386
387 operations.append(&mut overlay);
388 }
389}
390
391#[cfg(test)]
392#[path = "tests/renderer_tests.rs"]
393mod tests;