use crate::composite::composite_at;
use crate::geometry::{Constraints, Rect, Transform, Vec2};
use crate::raster::{PixelFormat, RasterComponent, RasterImage, RasterResidency, Resolution};
use crate::render_context::{CompositeInput, RenderContext};
use crate::vector::{Group, Node, VectorComponent, VectorGraphic};
#[crate::component(vector)]
#[derive(Clone, PartialEq, Hash)]
pub struct VectorLayer {
#[children(each = child)]
pub children: Vec<Box<dyn VectorComponent>>,
pub size: Vec2,
}
impl VectorLayer {
pub fn new(size: Vec2) -> Self {
Self {
size,
children: Vec::new(),
}
}
pub fn add(&mut self, child: impl Into<Box<dyn VectorComponent>>) -> &mut Self {
self.children.push(child.into());
self
}
}
impl VectorComponent for VectorLayer {
fn layout(&self, constraints: Constraints) -> Vec2 {
constraints.constrain(self.size)
}
fn render(&self, size: Vec2) -> VectorGraphic {
render_vector_children(
&self.children,
Rect {
origin: Vec2::ZERO,
size,
},
Constraints::loose(size),
)
}
}
pub(crate) fn vector_children_bounds(children: &[Box<dyn VectorComponent>]) -> Rect {
let mut iter = children.iter().map(|child| {
let child_size = child.layout(Constraints::UNBOUNDED);
child.paint_bounds(child_size)
});
let Some(first) = iter.next() else {
return Rect {
origin: Vec2::ZERO,
size: Vec2::ZERO,
};
};
iter.fold(first, union_rect)
}
pub(crate) fn render_vector_children(
children: &[Box<dyn VectorComponent>],
view_box: Rect,
child_constraints: Constraints,
) -> VectorGraphic {
let nodes: Vec<Node> = children
.iter()
.filter_map(|child| {
let child_size = child.layout(child_constraints);
let node = child.render(child_size).root;
(!node.is_empty()).then_some(node)
})
.collect();
VectorGraphic {
view_box,
root: Node::Group(Group {
transform: Transform::IDENTITY,
opacity: 1.0,
children: nodes,
}),
}
}
#[crate::component(raster)]
#[derive(Clone, PartialEq, Hash)]
pub struct Layer {
#[children(each = child)]
pub children: Vec<Box<dyn RasterComponent>>,
pub size: Vec2,
}
impl Layer {
pub fn new(size: Vec2) -> Self {
Self {
size,
children: Vec::new(),
}
}
pub fn add(&mut self, child: impl Into<Box<dyn RasterComponent>>) -> &mut Self {
self.children.push(child.into());
self
}
}
impl RasterComponent for Layer {
fn layout(&self, constraints: Constraints) -> Vec2 {
constraints.constrain(self.size)
}
fn paint_bounds(&self, size: Vec2) -> Rect {
let child_constraints = Constraints::loose(size);
let mut bounds = Rect {
origin: Vec2::ZERO,
size,
};
for child in &self.children {
let child_size = child.layout(child_constraints);
bounds = union_rect(bounds, child.paint_bounds(child_size));
}
bounds
}
fn render(
&self,
size: Vec2,
target: Resolution,
residency: RasterResidency,
ctx: &mut dyn RenderContext,
) -> RasterImage {
let paint_rect = self.paint_bounds(size);
let child_constraints = Constraints::loose(size);
let placed: Vec<(Vec2, Vec2, &dyn RasterComponent)> = self
.children
.iter()
.map(|child| {
let child_size = child.layout(child_constraints);
(Vec2::ZERO, child_size, child.as_ref())
})
.collect();
composite_children(paint_rect, target, &placed, residency, ctx)
}
}
pub(crate) fn raster_children_bounds(children: &[Box<dyn RasterComponent>]) -> Rect {
let mut iter = children.iter().map(|child| {
let child_size = child.layout(Constraints::UNBOUNDED);
child.paint_bounds(child_size)
});
let Some(first) = iter.next() else {
return Rect {
origin: Vec2::ZERO,
size: Vec2::ZERO,
};
};
iter.fold(first, union_rect)
}
pub(crate) fn composite_children(
paint_rect: Rect,
target: Resolution,
placed: &[(Vec2, Vec2, &dyn RasterComponent)],
residency: RasterResidency,
ctx: &mut dyn RenderContext,
) -> RasterImage {
let scale_x = target.width as f32 / paint_rect.size.0;
let scale_y = target.height as f32 / paint_rect.size.1;
let gpu_available = ctx.prefers_gpu() && ctx.gpu_backend().is_some();
let child_residency = if gpu_available {
RasterResidency::Gpu
} else {
RasterResidency::Cpu
};
let mut rendered = Vec::with_capacity(placed.len());
for (position, child_size, child) in placed {
let bounds = child.paint_bounds(*child_size);
let child_px_w = (bounds.size.0 * scale_x).round().max(1.0) as u32;
let child_px_h = (bounds.size.1 * scale_y).round().max(1.0) as u32;
let paint_x = position.0 + bounds.origin.0 - paint_rect.origin.0;
let paint_y = position.1 + bounds.origin.1 - paint_rect.origin.1;
let offset_x = (paint_x * scale_x).round() as i32;
let offset_y = (paint_y * scale_y).round() as i32;
let image = ctx.render(
*child,
*child_size,
Resolution::new(child_px_w, child_px_h),
child_residency,
);
let image = ctx.ensure_residency(image, child_residency);
rendered.push((image, offset_x, offset_y));
}
if gpu_available
&& rendered
.iter()
.all(|(image, _, _)| image.residency() == RasterResidency::Gpu)
{
let inputs: Vec<CompositeInput<'_>> = rendered
.iter()
.map(|(image, offset_x, offset_y)| CompositeInput {
image,
offset_x: *offset_x,
offset_y: *offset_y,
})
.collect();
if let Some(gpu) = ctx.gpu_backend() {
if let Some(image) = gpu.composite(target, &inputs) {
return ctx.ensure_residency(image, residency);
}
}
}
let mut accum = vec![0u8; (target.width as usize) * (target.height as usize) * 4];
for (image, offset_x, offset_y) in rendered {
let image = ctx.readback(image);
composite_at(&mut accum, target, &image, offset_x, offset_y);
}
let image = RasterImage::cpu(target.width, target.height, PixelFormat::Rgba8, accum);
ctx.ensure_residency(image, residency)
}
pub(crate) fn union_rect(a: Rect, b: Rect) -> Rect {
let a_end = Vec2(a.origin.0 + a.size.0, a.origin.1 + a.size.1);
let b_end = Vec2(b.origin.0 + b.size.0, b.origin.1 + b.size.1);
let origin = Vec2(a.origin.0.min(b.origin.0), a.origin.1.min(b.origin.1));
let end = Vec2(a_end.0.max(b_end.0), a_end.1.max(b_end.1));
Rect {
origin,
size: Vec2(end.0 - origin.0, end.1 - origin.1),
}
}
pub(crate) fn translate_rect(r: Rect, delta: Vec2) -> Rect {
Rect {
origin: Vec2(r.origin.0 + delta.0, r.origin.1 + delta.1),
size: r.size,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
use crate::placement::VectorPlacement;
use crate::raster::CpuRasterImage;
use crate::render_context::{DropShadowInput, GpuPreference, GpuRasterBackend, OutlineInput};
use crate::shapes::Rectangle;
use crate::vector::{Node, Paint};
fn rect(w: f32, h: f32) -> Rectangle {
Rectangle {
size: Vec2(w, h),
fill: Paint::Solid(Color::rgb_u8(0, 0, 0)).into(),
stroke: None,
}
}
#[test]
fn vector_layer_fixed_size_unchanged() {
let layer = VectorLayer {
size: Vec2(500.0, 300.0),
children: vec![rect(80.0, 40.0).place_at(Vec2(10.0, 20.0)).into()],
};
assert_eq!(layer.layout(Constraints::UNBOUNDED), Vec2(500.0, 300.0));
}
#[test]
fn vector_layer_prunes_empty_child_nodes() {
let invisible = Rectangle {
size: Vec2(10.0, 10.0),
fill: Paint::Solid(Color::rgba_u8(255, 0, 0, 0)).into(),
stroke: None,
};
let layer = VectorLayer {
size: Vec2(100.0, 100.0),
children: vec![invisible.into(), rect(10.0, 10.0).into()],
};
let graphic = layer.render(Vec2(100.0, 100.0));
let Node::Group(root) = graphic.root else {
panic!("vector layer should render a root group");
};
assert_eq!(root.children.len(), 1);
assert!(matches!(root.children[0], Node::Path(_)));
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct DummyRaster;
impl RasterComponent for DummyRaster {
fn layout(&self, constraints: Constraints) -> Vec2 {
constraints.constrain(Vec2(1.0, 1.0))
}
fn render(
&self,
_size: Vec2,
_target: Resolution,
_residency: RasterResidency,
_ctx: &mut dyn RenderContext,
) -> RasterImage {
panic!("test context intercepts renders")
}
}
#[derive(Default)]
struct FakeGpu {
composites: usize,
}
impl GpuRasterBackend for FakeGpu {
fn upload(&mut self, image: &CpuRasterImage) -> Option<RasterImage> {
Some(RasterImage::Gpu(crate::raster::GpuSurface::new(
image.width,
image.height,
image.format,
"test",
std::sync::Arc::new(image.clone()),
)))
}
fn composite(
&mut self,
target: Resolution,
_inputs: &[CompositeInput<'_>],
) -> Option<RasterImage> {
self.composites += 1;
let image = CpuRasterImage::new(
target.width,
target.height,
PixelFormat::Rgba8,
vec![7u8; (target.width as usize) * (target.height as usize) * 4],
);
Some(RasterImage::Gpu(crate::raster::GpuSurface::new(
target.width,
target.height,
PixelFormat::Rgba8,
"test",
std::sync::Arc::new(image),
)))
}
fn drop_shadow(&mut self, _input: DropShadowInput<'_>) -> Option<RasterImage> {
None
}
fn outline(&mut self, _input: OutlineInput<'_>) -> Option<RasterImage> {
None
}
fn rasterize(
&mut self,
_graphic: &VectorGraphic,
_target: Resolution,
) -> Option<RasterImage> {
None
}
fn solid_fill(&mut self, _target: Resolution, _color: Color) -> Option<RasterImage> {
None
}
fn temporal_average(
&mut self,
_target: Resolution,
_frames: &[&RasterImage],
_total: u32,
) -> Option<RasterImage> {
None
}
fn readback(&mut self, _image: RasterImage) -> Option<CpuRasterImage> {
None
}
}
struct FakeContext {
image: RasterImage,
gpu: FakeGpu,
renders: usize,
readbacks: usize,
}
impl FakeContext {
fn new() -> Self {
Self {
image: RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![1, 2, 3, 255]),
gpu: FakeGpu::default(),
renders: 0,
readbacks: 0,
}
}
}
impl RenderContext for FakeContext {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn gpu_preference(&self) -> GpuPreference {
GpuPreference::PreferGpu
}
fn gpu_backend(&mut self) -> Option<&mut dyn GpuRasterBackend> {
Some(&mut self.gpu)
}
fn render(
&mut self,
_component: &dyn RasterComponent,
_size: Vec2,
_target: Resolution,
_residency: RasterResidency,
) -> RasterImage {
self.renders += 1;
self.image.clone()
}
fn readback(&mut self, image: RasterImage) -> CpuRasterImage {
self.readbacks += 1;
match image {
RasterImage::Cpu(image) => image,
RasterImage::Gpu(surface) => surface
.downcast_handle::<CpuRasterImage>()
.expect("fake GPU surface contains a CPU image")
.clone(),
}
}
}
#[test]
fn composite_children_recomposites_same_input_storage() {
let child = DummyRaster;
let placed = [(Vec2::ZERO, Vec2(1.0, 1.0), &child as &dyn RasterComponent)];
let paint_rect = Rect {
origin: Vec2::ZERO,
size: Vec2(1.0, 1.0),
};
let mut ctx = FakeContext::new();
let first = composite_children(
paint_rect,
Resolution::new(1, 1),
&placed,
RasterResidency::Gpu,
&mut ctx,
);
let second = composite_children(
paint_rect,
Resolution::new(1, 1),
&placed,
RasterResidency::Gpu,
&mut ctx,
);
assert_eq!(ctx.readback(first).pixels.as_ref(), &[7, 7, 7, 7]);
assert_eq!(ctx.readback(second).pixels.as_ref(), &[7, 7, 7, 7]);
assert_eq!(ctx.renders, 2);
assert_eq!(ctx.gpu.composites, 2);
assert_eq!(ctx.readbacks, 2);
}
#[test]
fn composite_children_reads_gpu_result_back_for_cpu_consumer() {
let child = DummyRaster;
let placed = [(Vec2::ZERO, Vec2(1.0, 1.0), &child as &dyn RasterComponent)];
let paint_rect = Rect {
origin: Vec2::ZERO,
size: Vec2(1.0, 1.0),
};
let mut ctx = FakeContext::new();
let image = composite_children(
paint_rect,
Resolution::new(1, 1),
&placed,
RasterResidency::Cpu,
&mut ctx,
);
assert_eq!(image.into_cpu().unwrap().pixels.as_ref(), &[7, 7, 7, 7]);
assert_eq!(ctx.gpu.composites, 1);
assert_eq!(ctx.readbacks, 1);
}
}