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: primitive.translate(rect.x, rect.y),
192 });
193 }
194 }
195 ops
196}
197
198impl HeadlessRenderer {
199 pub fn render_from_applier(
202 &self,
203 applier: &mut MemoryApplier,
204 root: NodeId,
205 ) -> RecordedRenderScene {
206 let mut operations = Vec::new();
207 let mut child_stack = Vec::new();
208 self.render_node_from_applier(
209 applier,
210 root,
211 Point::default(),
212 &mut operations,
213 &mut child_stack,
214 );
215 RecordedRenderScene::new(operations)
216 }
217
218 fn render_node_from_applier(
223 &self,
224 applier: &mut MemoryApplier,
225 node_id: NodeId,
226 parent_offset: Point,
227 operations: &mut Vec<RenderOp>,
228 child_stack: &mut Vec<NodeId>,
229 ) {
230 let first_child = child_stack.len();
231 let Ok(Some((layout_state, modifier_slices))) =
232 applier.with_node::<LayoutNode, _>(node_id, |node| {
233 let state = node.layout_state();
234 state.is_placed().then(|| {
235 child_stack.extend_from_slice(&node.children);
236 (state, node.modifier_slices_snapshot())
237 })
238 })
239 else {
240 return;
241 };
242
243 let abs_x = parent_offset.x + layout_state.position().x;
244 let abs_y = parent_offset.y + layout_state.position().y;
245
246 let rect = Rect {
247 x: abs_x,
248 y: abs_y,
249 width: layout_state.size().width,
250 height: layout_state.size().height,
251 };
252
253 let size = Size {
254 width: rect.width,
255 height: rect.height,
256 };
257
258 operations.extend(collect_primitives_from_commands(
259 node_id,
260 rect,
261 size,
262 modifier_slices.draw_commands(),
263 PaintLayer::Behind,
264 ));
265
266 if let Some(text) = modifier_slices.text_content() {
267 operations.push(RenderOp::Text {
268 node_id,
269 rect,
270 value: text.to_string(),
271 });
272 }
273
274 let child_offset = Point {
275 x: abs_x + layout_state.content_offset.x,
276 y: abs_y + layout_state.content_offset.y,
277 };
278
279 for index in first_child..child_stack.len() {
280 let child_id = child_stack[index];
281 self.render_node_from_applier(applier, child_id, child_offset, operations, child_stack);
282 }
283 child_stack.truncate(first_child);
284
285 operations.extend(collect_primitives_from_commands(
286 node_id,
287 rect,
288 size,
289 modifier_slices.draw_commands(),
290 PaintLayer::Overlay,
291 ));
292 }
293}
294
295#[cfg(test)]
296#[path = "tests/renderer_tests.rs"]
297mod tests;