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