use crate::geometry::{Anchor, Constraints, Rect, Transform, Vec2};
use crate::layer::translate_rect;
use crate::vector::{Group, Node, VectorComponent, VectorGraphic};
use crate::Keyable;
#[derive(Debug, Clone, Copy, Keyable)]
pub enum SnapTarget {
Point(Vec2),
Anchor(Anchor),
}
impl SnapTarget {
fn point(self, size: Vec2) -> Vec2 {
match self {
Self::Point(point) => point,
Self::Anchor(anchor) => anchor.point(size),
}
}
}
impl From<Vec2> for SnapTarget {
fn from(point: Vec2) -> Self {
Self::Point(point)
}
}
impl From<Anchor> for SnapTarget {
fn from(anchor: Anchor) -> Self {
Self::Anchor(anchor)
}
}
fn positioned_layout(
target: SnapTarget,
constraints: Constraints,
child_layout: impl FnOnce(Constraints) -> Vec2,
) -> Vec2 {
match target {
SnapTarget::Point(_) => child_layout(Constraints::UNBOUNDED),
SnapTarget::Anchor(_) => constraints.fill_size(),
}
}
fn resolved_position(
anchor: Anchor,
target: SnapTarget,
offset: Vec2,
size: Vec2,
child_size: Vec2,
) -> Vec2 {
child_size.anchored(anchor).snap_to(target.point(size)) + offset
}
fn resolved_paint_bounds(child_bounds: Rect, position: Vec2) -> Rect {
translate_rect(child_bounds, position)
}
#[derive(Clone, Keyable)]
pub struct Positioned {
pub child: Box<dyn VectorComponent>,
pub anchor: Anchor,
pub target: SnapTarget,
pub offset: Vec2,
}
impl Positioned {
pub fn new(
child: impl Into<Box<dyn VectorComponent>>,
anchor: Anchor,
target: impl Into<SnapTarget>,
) -> Self {
Self {
child: child.into(),
anchor,
target: target.into(),
offset: Vec2::ZERO,
}
}
pub fn offset(mut self, offset: Vec2) -> Self {
self.offset = offset;
self
}
fn child_size(&self, size: Vec2) -> Vec2 {
match self.target {
SnapTarget::Point(_) => size,
SnapTarget::Anchor(_) => self.child.layout(Constraints::loose(size)),
}
}
fn position(&self, size: Vec2, child_size: Vec2) -> Vec2 {
resolved_position(self.anchor, self.target, self.offset, size, child_size)
}
}
impl VectorComponent for Positioned {
fn layout(&self, constraints: Constraints) -> Vec2 {
positioned_layout(self.target, constraints, |c| self.child.layout(c))
}
fn paint_bounds(&self, size: Vec2) -> Rect {
let child_size = self.child_size(size);
let position = self.position(size, child_size);
resolved_paint_bounds(self.child.paint_bounds(child_size), position)
}
fn render(&self, size: Vec2) -> VectorGraphic {
let child_size = self.child_size(size);
let position = self.position(size, child_size);
let inner = self.child.render(child_size);
VectorGraphic {
view_box: resolved_paint_bounds(inner.view_box, position),
root: Node::Group(Group {
transform: Transform::translate(position),
opacity: 1.0,
children: vec![inner.root],
}),
}
}
}
impl From<Positioned> for Box<dyn VectorComponent> {
fn from(positioned: Positioned) -> Self {
Box::new(positioned)
}
}
pub trait VectorPlacement: VectorComponent + Sized + 'static {
fn place_at(self, position: Vec2) -> Positioned {
Positioned::new(self.boxed(), Anchor::TOP_LEFT, position)
}
fn anchored(self, anchor: Anchor) -> AnchoredVectorComponent<Self> {
AnchoredVectorComponent {
component: self,
anchor,
}
}
}
impl<T: VectorComponent + 'static> VectorPlacement for T {}
pub struct AnchoredVectorComponent<C: VectorComponent> {
component: C,
anchor: Anchor,
}
impl<C: VectorComponent + 'static> AnchoredVectorComponent<C> {
pub fn snap_to(self, target: impl Into<SnapTarget>) -> Positioned {
Positioned::new(self.component.boxed(), self.anchor, target)
}
}
pub mod raster {
use super::{positioned_layout, resolved_paint_bounds, resolved_position, SnapTarget};
use crate::geometry::{Anchor, Constraints, Rect, Vec2};
use crate::raster::{RasterComponent, RasterImage, RasterResidency, Resolution};
use crate::render_context::{CachePolicy, RenderContext};
use crate::Keyable;
#[derive(Clone, Keyable)]
pub struct Positioned {
pub child: Box<dyn RasterComponent>,
pub anchor: Anchor,
pub target: SnapTarget,
pub offset: Vec2,
}
impl Positioned {
pub fn new(
child: impl Into<Box<dyn RasterComponent>>,
anchor: Anchor,
target: impl Into<SnapTarget>,
) -> Self {
Self {
child: child.into(),
anchor,
target: target.into(),
offset: Vec2::ZERO,
}
}
pub fn offset(mut self, offset: Vec2) -> Self {
self.offset = offset;
self
}
fn child_size(&self, size: Vec2) -> Vec2 {
match self.target {
SnapTarget::Point(_) => size,
SnapTarget::Anchor(_) => self.child.layout(Constraints::loose(size)),
}
}
fn position(&self, size: Vec2, child_size: Vec2) -> Vec2 {
resolved_position(self.anchor, self.target, self.offset, size, child_size)
}
}
impl RasterComponent for Positioned {
fn layout(&self, constraints: Constraints) -> Vec2 {
positioned_layout(self.target, constraints, |c| self.child.layout(c))
}
fn paint_bounds(&self, size: Vec2) -> Rect {
let child_size = self.child_size(size);
let position = self.position(size, child_size);
resolved_paint_bounds(self.child.paint_bounds(child_size), position)
}
fn cache_policy(&self) -> CachePolicy {
CachePolicy::Transparent
}
fn render(
&self,
size: Vec2,
target: Resolution,
residency: RasterResidency,
ctx: &mut dyn RenderContext,
) -> RasterImage {
let child_size = self.child_size(size);
ctx.render(self.child.as_ref(), child_size, target, residency)
}
}
impl From<Positioned> for Box<dyn RasterComponent> {
fn from(positioned: Positioned) -> Self {
Box::new(positioned)
}
}
pub trait RasterPlacement: RasterComponent + Sized + 'static {
fn place_at(self, position: Vec2) -> Positioned {
Positioned::new(self.boxed(), Anchor::TOP_LEFT, position)
}
fn anchored(self, anchor: Anchor) -> AnchoredRasterComponent<Self> {
AnchoredRasterComponent {
component: self,
anchor,
}
}
}
impl<T: RasterComponent + 'static> RasterPlacement for T {}
pub struct AnchoredRasterComponent<C: RasterComponent> {
component: C,
anchor: Anchor,
}
impl<C: RasterComponent + 'static> AnchoredRasterComponent<C> {
pub fn snap_to(self, target: impl Into<SnapTarget>) -> Positioned {
Positioned::new(self.component.boxed(), self.anchor, target)
}
}
}
pub use raster::RasterPlacement;
#[cfg(test)]
mod tests {
use super::raster::RasterPlacement;
use super::*;
use crate::color::Color;
use crate::raster::{PixelFormat, RasterComponent, RasterImage, RasterResidency, Resolution};
use crate::render_context::{PassThrough, RenderContext};
use crate::shapes::Circle;
use crate::vector::{Paint, Stroke};
fn big_circle() -> Circle {
Circle::builder()
.radius(720.0)
.fill(Paint::Solid(Color::rgb_u8(0, 0, 0)))
.build()
}
fn root_translation(graphic: &VectorGraphic) -> Vec2 {
let Node::Group(group) = &graphic.root else {
panic!("positioned vector should render a translating group");
};
Vec2(group.transform.tx, group.transform.ty)
}
#[test]
fn place_at_is_top_left_point_with_zero_offset() {
let placed = big_circle().place_at(Vec2(12.0, 34.0));
assert_eq!(placed.anchor, Anchor::TOP_LEFT);
assert_eq!(placed.target, SnapTarget::Point(Vec2(12.0, 34.0)));
assert_eq!(placed.offset, Vec2::ZERO);
}
#[test]
fn placed_child_is_measured_unbounded() {
let placed = big_circle().place_at(Vec2::ZERO);
let size = placed.layout(Constraints::loose(Vec2(1920.0, 1080.0)));
assert_eq!(size, Vec2(1440.0, 1440.0));
}
#[test]
fn point_target_resolves_from_unbounded_child_and_translates_paint_bounds() {
let placed = big_circle()
.anchored(Anchor::CENTER)
.snap_to(Vec2(960.0, 540.0))
.offset(Vec2(13.0, 9.0));
let size = placed.layout(Constraints::loose(Vec2(1920.0, 1080.0)));
assert_eq!(size, Vec2(1440.0, 1440.0));
let expected = Rect {
origin: Vec2(253.0, -171.0),
size,
};
assert_eq!(placed.paint_bounds(size), expected);
let graphic = placed.render(size);
assert_eq!(graphic.view_box, expected);
assert_eq!(root_translation(&graphic), expected.origin);
}
#[test]
fn point_target_preserves_child_stroke_outset_while_translating_bounds() {
let placed = Circle::builder()
.radius(10.0)
.stroke(Stroke::new(Paint::Solid(Color::rgb_u8(0, 0, 0)), 4.0))
.build()
.anchored(Anchor::CENTER)
.snap_to(Vec2(50.0, 40.0))
.offset(Vec2(3.0, -4.0));
let size = placed.layout(Constraints::UNBOUNDED);
let expected = Rect {
origin: Vec2(41.0, 24.0),
size: Vec2(24.0, 24.0),
};
assert_eq!(placed.paint_bounds(size), expected);
let graphic = placed.render(size);
assert_eq!(graphic.view_box, expected);
assert_eq!(root_translation(&graphic), Vec2(43.0, 26.0));
}
#[test]
fn anchor_target_fills_finite_constraints_and_collapses_unbounded_axes() {
let placed = Circle::builder()
.radius(10.0)
.fill(Paint::Solid(Color::rgb_u8(0, 0, 0)))
.build()
.anchored(Anchor::CENTER)
.snap_to(Anchor::BOTTOM_RIGHT);
assert_eq!(
placed.layout(Constraints::tight(Vec2(100.0, 60.0))),
Vec2(100.0, 60.0)
);
assert_eq!(
placed.layout(Constraints::loose(Vec2(100.0, 60.0))),
Vec2(100.0, 60.0)
);
assert_eq!(placed.layout(Constraints::UNBOUNDED), Vec2::ZERO);
assert_eq!(
placed.layout(Constraints::loose(Vec2(f32::INFINITY, 60.0))),
Vec2(0.0, 60.0)
);
}
#[test]
fn anchor_target_resolves_against_own_size_then_applies_offset() {
let placed = Circle::builder()
.radius(10.0)
.fill(Paint::Solid(Color::rgb_u8(0, 0, 0)))
.build()
.anchored(Anchor::CENTER)
.snap_to(Anchor::BOTTOM_RIGHT)
.offset(Vec2(-5.0, 3.0));
let size = Vec2(100.0, 60.0);
let expected = Rect {
origin: Vec2(85.0, 53.0),
size: Vec2(20.0, 20.0),
};
assert_eq!(placed.paint_bounds(size), expected);
let graphic = placed.render(size);
assert_eq!(graphic.view_box, expected);
assert_eq!(root_translation(&graphic), expected.origin);
}
#[derive(Clone, PartialEq, Hash)]
struct FixedRaster;
impl RasterComponent for FixedRaster {
fn layout(&self, constraints: Constraints) -> Vec2 {
constraints.constrain(Vec2(2000.0, 2000.0))
}
fn render(
&self,
_s: Vec2,
t: Resolution,
_residency: RasterResidency,
_ctx: &mut dyn RenderContext,
) -> RasterImage {
let bytes = (t.width as usize) * (t.height as usize) * 4;
RasterImage::cpu(t.width, t.height, PixelFormat::Rgba8, vec![0; bytes])
}
}
#[test]
fn raster_placed_child_is_measured_unbounded() {
let placed = FixedRaster
.place_at(Vec2(7.0, 11.0))
.offset(Vec2(3.0, -2.0));
let size = placed.layout(Constraints::loose(Vec2(100.0, 100.0)));
assert_eq!(size, Vec2(2000.0, 2000.0));
assert_eq!(
placed.paint_bounds(size),
Rect {
origin: Vec2(10.0, 9.0),
size,
}
);
}
#[derive(Clone, PartialEq, Hash)]
struct SmallRaster;
impl RasterComponent for SmallRaster {
fn layout(&self, constraints: Constraints) -> Vec2 {
constraints.constrain(Vec2(20.0, 10.0))
}
fn render(
&self,
size: Vec2,
target: Resolution,
_residency: RasterResidency,
_ctx: &mut dyn RenderContext,
) -> RasterImage {
let pixel = [size.0 as u8, size.1 as u8, 0, 255];
RasterImage::cpu(
target.width,
target.height,
PixelFormat::Rgba8,
pixel.repeat((target.width * target.height) as usize),
)
}
}
#[test]
fn raster_anchor_target_uses_box_for_position_but_child_size_for_pixels() {
let placed = SmallRaster
.anchored(Anchor::CENTER)
.snap_to(Anchor::BOTTOM_RIGHT)
.offset(Vec2(-5.0, 3.0));
let size = placed.layout(Constraints::tight(Vec2(100.0, 60.0)));
assert_eq!(
placed.paint_bounds(size),
Rect {
origin: Vec2(85.0, 58.0),
size: Vec2(20.0, 10.0),
}
);
let image = placed
.render(
size,
Resolution::new(1, 1),
RasterResidency::Cpu,
&mut PassThrough,
)
.into_cpu()
.expect("small raster stays on CPU");
assert_eq!(image.pixels.as_ref(), &[20, 10, 0, 255]);
}
}