use std::f32::consts::SQRT_2;
use image::{GenericImageView, Rgba};
use taffy::{Point, Rect, Size};
use zeno::{Command, Fill, PathBuilder};
use crate::{
layout::style::{Affine, BlendMode, BorderStyle, Color, ImageScalingAlgorithm, Sides, SpacePair},
rendering::{
Canvas, RenderContext, apply_mask_alpha_to_pixel, blend_pixel, mask_index_from_coord,
overlay_area, sample_transformed_pixel,
},
};
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct BorderProperties {
pub width: Rect<f32>,
pub color: Color,
pub radius: Sides<SpacePair<f32>>,
pub style: BorderStyle,
pub image_rendering: ImageScalingAlgorithm,
}
impl BorderProperties {
const PATH_COMMANDS_AMOUNT: usize = 10;
pub const fn zero() -> Self {
Self {
width: Rect::ZERO,
color: Color([0, 0, 0, 255]),
radius: Sides([SpacePair::from_single(0.0); 4]),
style: BorderStyle::None,
image_rendering: ImageScalingAlgorithm::Auto,
}
}
pub fn resolve_radius_part(
context: &RenderContext,
border_box: Size<f32>,
) -> Sides<SpacePair<f32>> {
let resolved = context.style.resolved_border_radius();
let top_left = resolved.top.to_px(&context.sizing, border_box);
let top_right = resolved.right.to_px(&context.sizing, border_box);
let bottom_right = resolved.bottom.to_px(&context.sizing, border_box);
let bottom_left = resolved.left.to_px(&context.sizing, border_box);
Sides([top_left, top_right, bottom_right, bottom_left])
}
pub fn from_context(
context: &RenderContext,
border_box: Size<f32>,
border_width: Rect<f32>,
) -> Self {
Self {
width: border_width,
color: context
.style
.border_color
.unwrap_or(context.style.border.color)
.resolve(context.current_color),
radius: Self::resolve_radius_part(context, border_box),
style: context
.style
.border_style
.unwrap_or(context.style.border.style),
image_rendering: context.style.image_rendering,
}
}
#[inline]
pub fn is_zero(&self) -> bool {
const ZERO: Sides<SpacePair<f32>> = Sides([SpacePair::from_single(0.0); 4]);
self.radius == ZERO
}
pub fn expand_by(&mut self, amount: Rect<f32>) {
if amount == Rect::ZERO {
return;
}
self.radius.0[0].x = (self.radius.0[0].x + amount.left).max(0.0);
self.radius.0[0].y = (self.radius.0[0].y + amount.top).max(0.0);
self.radius.0[1].x = (self.radius.0[1].x + amount.right).max(0.0);
self.radius.0[1].y = (self.radius.0[1].y + amount.top).max(0.0);
self.radius.0[2].x = (self.radius.0[2].x + amount.right).max(0.0);
self.radius.0[2].y = (self.radius.0[2].y + amount.bottom).max(0.0);
self.radius.0[3].x = (self.radius.0[3].x + amount.left).max(0.0);
self.radius.0[3].y = (self.radius.0[3].y + amount.bottom).max(0.0);
}
pub fn inset_by_border_width(&mut self) {
self.expand_by(self.width.map(|size| -size))
}
pub fn append_mask_commands(
&self,
path: &mut Vec<Command>,
border_box: Size<f32>,
offset: Point<f32>,
) {
path.reserve_exact(BorderProperties::PATH_COMMANDS_AMOUNT);
const KAPPA: f32 = 4.0 / 3.0 * (SQRT_2 - 1.0);
let scale = 1.0f32
.min(
if self.radius.0[0].x + self.radius.0[1].x > border_box.width {
border_box.width / (self.radius.0[0].x + self.radius.0[1].x)
} else {
1.0
},
)
.min(
if self.radius.0[3].x + self.radius.0[2].x > border_box.width {
border_box.width / (self.radius.0[3].x + self.radius.0[2].x)
} else {
1.0
},
)
.min(
if self.radius.0[0].y + self.radius.0[3].y > border_box.height {
border_box.height / (self.radius.0[0].y + self.radius.0[3].y)
} else {
1.0
},
)
.min(
if self.radius.0[1].y + self.radius.0[2].y > border_box.height {
border_box.height / (self.radius.0[1].y + self.radius.0[2].y)
} else {
1.0
},
);
path.move_to((offset.x + (self.radius.0[0].x * scale).max(0.0), offset.y));
path.line_to((
offset.x + border_box.width - (self.radius.0[1].x * scale).max(0.0),
offset.y,
));
if self.radius.0[1].x > 0.0 && self.radius.0[1].y > 0.0 {
let rx = self.radius.0[1].x * scale;
let ry = self.radius.0[1].y * scale;
path.curve_to(
(offset.x + border_box.width - rx * (1.0 - KAPPA), offset.y),
(offset.x + border_box.width, offset.y + ry * (1.0 - KAPPA)),
(offset.x + border_box.width, offset.y + ry),
);
} else {
path.line_to((offset.x + border_box.width, offset.y));
}
path.line_to((
offset.x + border_box.width,
offset.y + border_box.height - (self.radius.0[2].y * scale).max(0.0),
));
if self.radius.0[2].x > 0.0 && self.radius.0[2].y > 0.0 {
let rx = self.radius.0[2].x * scale;
let ry = self.radius.0[2].y * scale;
path.curve_to(
(
offset.x + border_box.width,
offset.y + border_box.height - ry * (1.0 - KAPPA),
),
(
offset.x + border_box.width - rx * (1.0 - KAPPA),
offset.y + border_box.height,
),
(
offset.x + border_box.width - rx,
offset.y + border_box.height,
),
);
} else {
path.line_to((offset.x + border_box.width, offset.y + border_box.height));
}
path.line_to((
offset.x + (self.radius.0[3].x * scale).max(0.0),
offset.y + border_box.height,
));
if self.radius.0[3].x > 0.0 && self.radius.0[3].y > 0.0 {
let rx = self.radius.0[3].x * scale;
let ry = self.radius.0[3].y * scale;
path.curve_to(
(offset.x + rx * (1.0 - KAPPA), offset.y + border_box.height),
(offset.x, offset.y + border_box.height - ry * (1.0 - KAPPA)),
(offset.x, offset.y + border_box.height - ry),
);
} else {
path.line_to((offset.x, offset.y + border_box.height));
}
path.line_to((offset.x, offset.y + (self.radius.0[0].y * scale).max(0.0)));
if self.radius.0[0].x > 0.0 && self.radius.0[0].y > 0.0 {
let rx = self.radius.0[0].x * scale;
let ry = self.radius.0[0].y * scale;
path.curve_to(
(offset.x, offset.y + ry * (1.0 - KAPPA)),
(offset.x + rx * (1.0 - KAPPA), offset.y),
(offset.x + rx, offset.y),
);
} else {
path.line_to((offset.x, offset.y));
}
path.close();
}
pub(crate) fn draw<I: GenericImageView<Pixel = Rgba<u8>>>(
mut self,
canvas: &mut Canvas,
border_box: Size<f32>,
transform: Affine,
clip_image: Option<&I>,
) {
if let Some(clip_image) = &clip_image {
assert_eq!(
clip_image.dimensions(),
(border_box.width as u32, border_box.height as u32)
);
}
if self.style == BorderStyle::None
|| (self.width.left == 0.0
&& self.width.right == 0.0
&& self.width.top == 0.0
&& self.width.bottom == 0.0)
{
return;
}
let mut paths = Vec::with_capacity(BorderProperties::PATH_COMMANDS_AMOUNT * 2);
self.append_mask_commands(&mut paths, border_box, Point::ZERO);
self.inset_by_border_width();
self.append_mask_commands(
&mut paths,
border_box
- Size {
width: self.width.left + self.width.right,
height: self.width.top + self.width.bottom,
},
Point {
x: self.width.left,
y: self.width.top,
},
);
let (mask, placement) = canvas.mask_memory.render(
&paths,
Some(transform),
Some(Fill::EvenOdd.into()),
&mut canvas.buffer_pool,
);
let Some(inverse) = transform.invert() else {
return;
};
overlay_area(
&mut canvas.image,
Point {
x: placement.left as f32,
y: placement.top as f32,
},
Size {
width: placement.width,
height: placement.height,
},
BlendMode::Normal,
&canvas.constrains,
|x, y| {
let alpha = mask[mask_index_from_coord(x, y, placement.width)];
let clip_image_pixel = clip_image.and_then(|image| {
let canvas_x = (x as i32 + placement.left) as f32;
let canvas_y = (y as i32 + placement.top) as f32;
sample_transformed_pixel(
image,
inverse,
self.image_rendering,
canvas_x,
canvas_y,
Point::ZERO,
)
});
let mut pixel = self.color.into();
if let Some(clip_image_pixel) = clip_image_pixel {
blend_pixel(&mut pixel, clip_image_pixel, BlendMode::Normal);
}
apply_mask_alpha_to_pixel(&mut pixel, alpha);
pixel
},
);
}
}