use crate::layout::{LayoutBox, LayoutNodeData, LayoutTree};
use crate::modifier::{DrawCommand as ModifierDrawCommand, Point, Rect, Size};
use crate::widgets::LayoutNode;
use cranpose_core::{MemoryApplier, NodeId};
use cranpose_ui_graphics::DrawPrimitive;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PaintLayer {
Behind,
Content,
Overlay,
}
#[derive(Clone, Debug, PartialEq)]
pub enum RenderOp {
Primitive {
node_id: NodeId,
layer: PaintLayer,
primitive: DrawPrimitive,
},
Text {
node_id: NodeId,
rect: Rect,
value: String,
},
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RecordedRenderScene {
operations: Vec<RenderOp>,
}
impl RecordedRenderScene {
pub fn new(operations: Vec<RenderOp>) -> Self {
Self { operations }
}
pub fn operations(&self) -> &[RenderOp] {
&self.operations
}
pub fn into_operations(self) -> Vec<RenderOp> {
self.operations
}
pub fn primitives_for(&self, layer: PaintLayer) -> impl Iterator<Item = &DrawPrimitive> {
self.operations.iter().filter_map(move |op| match op {
RenderOp::Primitive {
layer: op_layer,
primitive,
..
} if *op_layer == layer => Some(primitive),
_ => None,
})
}
}
#[derive(Default)]
pub struct HeadlessRenderer;
impl HeadlessRenderer {
pub fn new() -> Self {
Self
}
pub fn render(&self, tree: &LayoutTree) -> RecordedRenderScene {
let mut operations = Vec::new();
self.render_box(tree.root(), &mut operations);
RecordedRenderScene::new(operations)
}
#[allow(clippy::only_used_in_recursion)]
fn render_box(&self, layout: &LayoutBox, operations: &mut Vec<RenderOp>) {
let rect = layout.rect;
let (mut behind, mut overlay) = evaluate_modifier(layout.node_id, &layout.node_data, rect);
operations.append(&mut behind);
if let Some(text) = layout.node_data.modifier_slices().text_content() {
operations.push(RenderOp::Text {
node_id: layout.node_id,
rect,
value: text.to_string(),
});
}
for child in &layout.children {
self.render_box(child, operations);
}
operations.append(&mut overlay);
}
}
fn evaluate_modifier(
node_id: NodeId,
data: &LayoutNodeData,
rect: Rect,
) -> (Vec<RenderOp>, Vec<RenderOp>) {
let size = Size {
width: rect.width,
height: rect.height,
};
let behind = collect_primitives_from_commands(
node_id,
rect,
size,
data.modifier_slices().draw_commands(),
PaintLayer::Behind,
);
let overlay = collect_primitives_from_commands(
node_id,
rect,
size,
data.modifier_slices().draw_commands(),
PaintLayer::Overlay,
);
(behind, overlay)
}
fn collect_primitives_from_commands(
node_id: NodeId,
rect: Rect,
size: Size,
commands: &[ModifierDrawCommand],
layer: PaintLayer,
) -> Vec<RenderOp> {
let split_with_content = |primitives: Vec<DrawPrimitive>, layer| {
let Some(last_content_idx) = primitives
.iter()
.rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
else {
return if layer == PaintLayer::Overlay {
primitives
.into_iter()
.filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
.collect()
} else {
Vec::new()
};
};
primitives
.into_iter()
.enumerate()
.filter_map(|(index, primitive)| {
if matches!(primitive, DrawPrimitive::Content) {
return None;
}
let is_before = index < last_content_idx;
match layer {
PaintLayer::Behind if is_before => Some(primitive),
PaintLayer::Overlay if !is_before => Some(primitive),
_ => None,
}
})
.collect()
};
let mut ops = Vec::new();
for command in commands {
let primitives = match (layer, command) {
(PaintLayer::Behind, ModifierDrawCommand::Behind(func)) => func(size)
.into_iter()
.filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
.collect(),
(PaintLayer::Overlay, ModifierDrawCommand::Overlay(func)) => func(size)
.into_iter()
.filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
.collect(),
(PaintLayer::Behind | PaintLayer::Overlay, ModifierDrawCommand::WithContent(func)) => {
split_with_content(func(size), layer)
}
_ => Vec::new(),
};
for primitive in primitives {
ops.push(RenderOp::Primitive {
node_id,
layer,
primitive: translate_primitive(primitive, rect.x, rect.y),
});
}
}
ops
}
fn translate_primitive(primitive: DrawPrimitive, dx: f32, dy: f32) -> DrawPrimitive {
match primitive {
DrawPrimitive::Content => DrawPrimitive::Content,
DrawPrimitive::Blend {
primitive,
blend_mode,
} => DrawPrimitive::Blend {
primitive: Box::new(translate_primitive(*primitive, dx, dy)),
blend_mode,
},
DrawPrimitive::Rect { rect, brush } => DrawPrimitive::Rect {
rect: rect.translate(dx, dy),
brush,
},
DrawPrimitive::RoundRect { rect, brush, radii } => DrawPrimitive::RoundRect {
rect: rect.translate(dx, dy),
brush,
radii,
},
DrawPrimitive::Image {
rect,
image,
alpha,
color_filter,
src_rect,
} => DrawPrimitive::Image {
rect: rect.translate(dx, dy),
image,
alpha,
color_filter,
src_rect,
},
DrawPrimitive::Shadow(shadow) => {
use cranpose_ui_graphics::ShadowPrimitive;
DrawPrimitive::Shadow(match shadow {
ShadowPrimitive::Drop {
shape,
blur_radius,
blend_mode,
} => ShadowPrimitive::Drop {
shape: Box::new(translate_primitive(*shape, dx, dy)),
blur_radius,
blend_mode,
},
ShadowPrimitive::Inner {
fill,
cutout,
blur_radius,
blend_mode,
clip_rect,
} => ShadowPrimitive::Inner {
fill: Box::new(translate_primitive(*fill, dx, dy)),
cutout: Box::new(translate_primitive(*cutout, dx, dy)),
blur_radius,
blend_mode,
clip_rect: clip_rect.translate(dx, dy),
},
})
}
}
}
impl HeadlessRenderer {
pub fn render_from_applier(
&self,
applier: &mut MemoryApplier,
root: NodeId,
) -> RecordedRenderScene {
let mut operations = Vec::new();
self.render_node_from_applier(applier, root, Point::default(), &mut operations);
RecordedRenderScene::new(operations)
}
#[allow(clippy::only_used_in_recursion)]
fn render_node_from_applier(
&self,
applier: &mut MemoryApplier,
node_id: NodeId,
parent_offset: Point,
operations: &mut Vec<RenderOp>,
) {
let node_data = match applier.with_node::<LayoutNode, _>(node_id, |node| {
let state = node.layout_state();
let modifier_slices = node.modifier_slices_snapshot();
let children: Vec<NodeId> = node.children.clone();
(state, modifier_slices, children)
}) {
Ok(data) => data,
Err(_) => return, };
let (layout_state, modifier_slices, children) = node_data;
if !layout_state.is_placed {
return;
}
let abs_x = parent_offset.x + layout_state.position.x;
let abs_y = parent_offset.y + layout_state.position.y;
let rect = Rect {
x: abs_x,
y: abs_y,
width: layout_state.size.width,
height: layout_state.size.height,
};
let size = Size {
width: rect.width,
height: rect.height,
};
let mut behind = Vec::new();
let mut overlay = Vec::new();
behind.extend(collect_primitives_from_commands(
node_id,
rect,
size,
modifier_slices.draw_commands(),
PaintLayer::Behind,
));
overlay.extend(collect_primitives_from_commands(
node_id,
rect,
size,
modifier_slices.draw_commands(),
PaintLayer::Overlay,
));
operations.append(&mut behind);
if let Some(text) = modifier_slices.text_content() {
operations.push(RenderOp::Text {
node_id,
rect,
value: text.to_string(),
});
}
let child_offset = Point {
x: abs_x + layout_state.content_offset.x,
y: abs_y + layout_state.content_offset.y,
};
for child_id in children {
self.render_node_from_applier(applier, child_id, child_offset, operations);
}
operations.append(&mut overlay);
}
}
#[cfg(test)]
#[path = "tests/renderer_tests.rs"]
mod tests;