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