use std::fmt;
use crate::layout::style::{ToCss, properties::write_css_string, unexpected_token};
use cssparser::{Parser, Token, match_ignore_ascii_case};
use taffy::{AbsoluteAxis, Point, Rect, Size};
use crate::{
layout::style::{
Axis, BorderStyle, Color, CssDescriptorKind, CssSyntaxKind, CssToken, FromCss,
ImageScalingAlgorithm, Length, MakeComputed, ParseResult, Sides, SpacePair,
},
rendering::{
BorderProperties, BufferPool, Fill, PathBuilder, PathData, Placement, RenderContext, Sizing,
render_mask,
},
};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub enum FillRule {
#[default]
NonZero,
EvenOdd,
}
impl From<FillRule> for Fill {
fn from(value: FillRule) -> Self {
match value {
FillRule::EvenOdd => Fill::EvenOdd,
FillRule::NonZero => Fill::NonZero,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub enum ShapeRadius {
#[default]
ClosestSide,
FarthestSide,
Length(Length),
}
impl MakeComputed for ShapeRadius {
fn make_computed(&mut self, sizing: &Sizing) {
if let ShapeRadius::Length(length) = self {
length.make_computed(sizing);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ShapePosition(pub SpacePair<Length>);
impl MakeComputed for ShapePosition {
fn make_computed(&mut self, sizing: &Sizing) {
self.0.make_computed(sizing);
}
}
impl Default for ShapePosition {
fn default() -> Self {
Self(SpacePair::from_single(Length::Percentage(50.0)))
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct InsetShape {
pub inset: Sides<Length>,
pub border_radius: Option<Sides<Length>>,
}
impl MakeComputed for InsetShape {
fn make_computed(&mut self, sizing: &Sizing) {
self.inset.make_computed(sizing);
self.border_radius.make_computed(sizing);
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EllipseShape {
pub radius_x: ShapeRadius,
pub radius_y: ShapeRadius,
pub position: ShapePosition,
}
impl MakeComputed for EllipseShape {
fn make_computed(&mut self, sizing: &Sizing) {
self.radius_x.make_computed(sizing);
self.radius_y.make_computed(sizing);
self.position.make_computed(sizing);
}
}
pub type PolygonCoordinate = SpacePair<Length>;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct PolygonShape {
pub fill_rule: Option<FillRule>,
pub coordinates: Box<[PolygonCoordinate]>,
}
impl MakeComputed for PolygonShape {
fn make_computed(&mut self, sizing: &Sizing) {
self.coordinates.make_computed(sizing);
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct PathShape {
pub fill_rule: Option<FillRule>,
pub path: Box<str>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum BasicShape {
Inset(Box<InsetShape>),
Ellipse(Box<EllipseShape>),
Polygon(PolygonShape),
Path(PathShape),
}
impl MakeComputed for BasicShape {
fn make_computed(&mut self, sizing: &Sizing) {
match self {
BasicShape::Inset(shape) => shape.make_computed(sizing),
BasicShape::Ellipse(shape) => shape.make_computed(sizing),
BasicShape::Polygon(shape) => shape.make_computed(sizing),
BasicShape::Path(_) => {}
}
}
}
fn resolve_radius(radius: ShapeRadius, distance: Size<f32>, sizing: &Sizing, full: f32) -> f32 {
match radius {
ShapeRadius::ClosestSide => distance.width.min(distance.height),
ShapeRadius::FarthestSide => distance.width.max(distance.height),
ShapeRadius::Length(length) => length.to_px(sizing, full),
}
}
impl BasicShape {
pub(crate) fn fill_rule(&self) -> Option<FillRule> {
match self {
BasicShape::Polygon(shape) => shape.fill_rule,
BasicShape::Path(shape) => shape.fill_rule,
_ => None,
}
}
pub(crate) fn render_mask(
&self,
context: &RenderContext,
size: Size<f32>,
buffer_pool: &mut BufferPool,
) -> (Vec<u8>, Placement) {
let mut paths = Vec::new();
match self {
BasicShape::Inset(shape) => {
let inset: Rect<f32> = shape
.inset
.map_axis(|value, axis| {
value.to_px(
&context.sizing,
match axis {
Axis::Horizontal => size.width,
Axis::Vertical => size.height,
},
)
})
.into();
let border = BorderProperties {
width: Rect::zero(),
color: Rect {
top: Color::transparent(),
right: Color::transparent(),
bottom: Color::transparent(),
left: Color::transparent(),
},
radius: shape
.border_radius
.map(|radius| {
Sides(
radius
.0
.map(|corner| SpacePair::from_single(corner.to_px(&context.sizing, size.width))),
)
})
.unwrap_or_default(),
image_rendering: ImageScalingAlgorithm::Auto,
style: Rect {
top: BorderStyle::Solid,
right: BorderStyle::Solid,
bottom: BorderStyle::Solid,
left: BorderStyle::Solid,
},
};
border.append_mask_commands(
&mut paths,
Size {
width: size.width - inset.grid_axis_sum(AbsoluteAxis::Horizontal),
height: size.height - inset.grid_axis_sum(AbsoluteAxis::Vertical),
},
Point {
x: inset.left,
y: inset.top,
},
);
}
BasicShape::Ellipse(shape) => {
let distance = Size {
width: shape.position.0.x.to_px(&context.sizing, size.width),
height: shape.position.0.y.to_px(&context.sizing, size.height),
};
paths.add_ellipse(
(distance.width, distance.height),
resolve_radius(shape.radius_x, distance, &context.sizing, size.width),
resolve_radius(shape.radius_y, distance, &context.sizing, size.height),
);
}
BasicShape::Polygon(shape) => {
if !shape.coordinates.is_empty() {
let first = &shape.coordinates[0];
let first_x = first.x.to_px(&context.sizing, size.width);
let first_y = first.y.to_px(&context.sizing, size.height);
paths.move_to((first_x, first_y));
for coord in &shape.coordinates[1..] {
let x = coord.x.to_px(&context.sizing, size.width);
let y = coord.y.to_px(&context.sizing, size.height);
paths.line_to((x, y));
}
paths.close();
}
}
BasicShape::Path(shape) => {
paths.extend(shape.path.as_ref().commands());
}
}
render_mask(
&paths,
Some(context.transform),
Some(Fill::from(self.fill_rule().unwrap_or(context.style.clip_rule)).into()),
buffer_pool,
)
}
}
crate::layout::style::properties::declare_enum_from_css_impl!(
FillRule,
"nonzero" => FillRule::NonZero,
"evenodd" => FillRule::EvenOdd,
);
impl<'i> FromCss<'i> for ShapeRadius {
fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let location = parser.current_source_location();
if let Ok(length) = parser.try_parse(Length::from_css) {
return Ok(ShapeRadius::Length(length));
}
let ident = parser.expect_ident()?;
match_ignore_ascii_case! { &ident,
"closest-side" => Ok(ShapeRadius::ClosestSide),
"farthest-side" => Ok(ShapeRadius::FarthestSide),
_ => Err(unexpected_token!(location, &Token::Ident(ident.clone()))),
}
}
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Keyword("closest-side"),
CssToken::Keyword("farthest-side"),
CssToken::Syntax(CssSyntaxKind::Length),
];
}
impl<'i> FromCss<'i> for ShapePosition {
fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let first = Length::from_css(parser)?;
let second = parser
.try_parse(Length::from_css)
.unwrap_or(Length::Percentage(50.0));
Ok(ShapePosition(SpacePair::from_pair(first, second)))
}
const VALID_TOKENS: &'static [CssToken] = Length::<true>::VALID_TOKENS;
}
impl<'i> FromCss<'i> for BasicShape {
fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let location = parser.current_source_location();
let token = parser.next()?;
match token {
Token::Function(function) => {
match_ignore_ascii_case! { &function,
"inset" => parser.parse_nested_block(|input| {
let inset = Sides::from_css(input)?;
let border_radius = if input.try_parse(|input| input.expect_ident_matching("round")).is_ok() {
Some(Sides::from_css(input)?)
} else {
None
};
Ok(BasicShape::Inset(Box::new(InsetShape {
inset,
border_radius,
})))
}),
"circle" => parser.parse_nested_block(|input| {
let radius = input.try_parse(ShapeRadius::from_css).unwrap_or_default();
let position = if input.try_parse(|input| input.expect_ident_matching("at")).is_ok() {
ShapePosition::from_css(input)?
} else {
ShapePosition::default()
};
Ok(BasicShape::Ellipse(Box::new(EllipseShape { radius_x: radius, radius_y: radius, position })))
}),
"ellipse" => parser.parse_nested_block(|input| {
let radius_x = ShapeRadius::from_css(input)?;
let radius_y = input.try_parse(ShapeRadius::from_css).unwrap_or_default();
let position = if input.try_parse(|input| input.expect_ident_matching("at")).is_ok() {
ShapePosition::from_css(input)?
} else {
ShapePosition::default()
};
Ok(BasicShape::Ellipse(Box::new(EllipseShape { radius_x, radius_y, position })))
}),
"polygon" => parser.parse_nested_block(|input| {
let fill_rule = input.try_parse(FillRule::from_css).ok();
if fill_rule.is_some() {
input.expect_comma()?;
}
Ok(BasicShape::Polygon(PolygonShape {
fill_rule,
coordinates: input
.parse_comma_separated(PolygonCoordinate::from_css)?
.into_boxed_slice(),
}))
}),
"path" => parser.parse_nested_block(|input| {
let fill_rule = input.try_parse(FillRule::from_css).ok();
if fill_rule.is_some() {
input.expect_comma()?;
}
let path = input.expect_string()?.as_ref().into();
Ok(BasicShape::Path(PathShape {
fill_rule,
path,
}))
}),
_ => Err(unexpected_token!(location, token)),
}
}
_ => Err(unexpected_token!(location, token)),
}
}
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Descriptor(CssDescriptorKind::InsetFn),
CssToken::Descriptor(CssDescriptorKind::CircleFn),
CssToken::Descriptor(CssDescriptorKind::EllipseFn),
CssToken::Descriptor(CssDescriptorKind::PolygonFn),
CssToken::Descriptor(CssDescriptorKind::PathFn),
];
}
impl ToCss for ShapeRadius {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Self::ClosestSide => dest.write_str("closest-side"),
Self::FarthestSide => dest.write_str("farthest-side"),
Self::Length(l) => l.to_css(dest),
}
}
}
impl ToCss for ShapePosition {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
self.0.to_css(dest)
}
}
impl ToCss for BasicShape {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Self::Inset(shape) => {
dest.write_str("inset(")?;
shape.inset.to_css(dest)?;
if let Some(radius) = &shape.border_radius {
dest.write_str(" round ")?;
radius.to_css(dest)?;
}
dest.write_char(')')
}
Self::Ellipse(shape) => {
if shape.radius_x == shape.radius_y {
dest.write_str("circle(")?;
let mut has_radius = false;
if shape.radius_x != ShapeRadius::ClosestSide {
shape.radius_x.to_css(dest)?;
has_radius = true;
}
if shape.position != ShapePosition::default() {
if has_radius {
dest.write_char(' ')?;
}
dest.write_str("at ")?;
shape.position.to_css(dest)?;
}
dest.write_char(')')
} else {
dest.write_str("ellipse(")?;
shape.radius_x.to_css(dest)?;
dest.write_char(' ')?;
shape.radius_y.to_css(dest)?;
if shape.position != ShapePosition::default() {
dest.write_str(" at ")?;
shape.position.to_css(dest)?;
}
dest.write_char(')')
}
}
Self::Polygon(shape) => {
dest.write_str("polygon(")?;
if let Some(rule) = shape.fill_rule {
rule.to_css(dest)?;
dest.write_str(", ")?;
}
let mut first = true;
for coord in shape.coordinates.iter() {
if !first {
dest.write_str(", ")?;
}
coord.to_css(dest)?;
first = false;
}
dest.write_char(')')
}
Self::Path(shape) => {
dest.write_str("path(")?;
if let Some(rule) = shape.fill_rule {
rule.to_css(dest)?;
dest.write_str(", ")?;
}
write_css_string(dest, &shape.path)?;
dest.write_char(')')
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use Length::*;
#[test]
fn test_parse_inset_simple() {
assert_eq!(
BasicShape::from_str("inset(10px)"),
Ok(BasicShape::Inset(Box::new(InsetShape {
inset: Sides([Px(10.0); 4]),
border_radius: None,
})))
);
}
#[test]
fn test_parse_inset_four_values() {
assert_eq!(
BasicShape::from_str("inset(10px 20px 30px 40px)"),
Ok(BasicShape::Inset(Box::new(InsetShape {
inset: Sides([Px(10.0), Px(20.0), Px(30.0), Px(40.0)]),
border_radius: None,
})))
);
}
#[test]
fn test_parse_inset_with_border_radius() {
assert_eq!(
BasicShape::from_str("inset(10px round 5px)"),
Ok(BasicShape::Inset(Box::new(InsetShape {
inset: Sides::from(Px(10.0)),
border_radius: Some(Sides::from(Px(5.0))),
})))
);
}
#[test]
fn test_parse_inset_with_complex_border_radius() {
assert_eq!(
BasicShape::from_str("inset(10px 20px 30px 40px round 5px 10px 15px 20px)"),
Ok(BasicShape::Inset(Box::new(InsetShape {
inset: Sides([Px(10.0), Px(20.0), Px(30.0), Px(40.0)]),
border_radius: Some(Sides([Px(5.0), Px(10.0), Px(15.0), Px(20.0)])),
})))
);
}
#[test]
fn test_parse_circle_simple() {
assert_eq!(
BasicShape::from_str("circle(50px)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::Length(Px(50.0)),
radius_y: ShapeRadius::Length(Px(50.0)),
position: ShapePosition::default(),
})))
);
}
#[test]
fn test_parse_circle_with_position() {
assert_eq!(
BasicShape::from_str("circle(50px at 25% 75%)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::Length(Px(50.0)),
radius_y: ShapeRadius::Length(Px(50.0)),
position: ShapePosition(SpacePair {
x: Length::Percentage(25.0),
y: Length::Percentage(75.0),
}),
})))
);
}
#[test]
fn test_parse_circle_default_radius() {
assert_eq!(
BasicShape::from_str("circle(at 25% 75%)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::ClosestSide,
radius_y: ShapeRadius::ClosestSide,
position: ShapePosition(SpacePair {
x: Length::Percentage(25.0),
y: Length::Percentage(75.0),
}),
})))
);
}
#[test]
fn test_parse_ellipse_simple() {
assert_eq!(
BasicShape::from_str("ellipse(50px 30px)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::Length(Px(50.0)),
radius_y: ShapeRadius::Length(Px(30.0)),
position: ShapePosition::default(),
})))
);
}
#[test]
fn test_parse_ellipse_with_position() {
assert_eq!(
BasicShape::from_str("ellipse(50px 30px at 25% 75%)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::Length(Px(50.0)),
radius_y: ShapeRadius::Length(Px(30.0)),
position: ShapePosition(SpacePair {
x: Length::Percentage(25.0),
y: Length::Percentage(75.0),
}),
})))
);
}
#[test]
fn test_parse_polygon_triangle() {
assert!(matches!(
BasicShape::from_str("polygon(50% 0%, 0% 100%, 100% 100%)"),
Ok(BasicShape::Polygon(PolygonShape {
fill_rule: None,
coordinates: coords,
})) if coords.len() == 3 &&
coords[0] == SpacePair { x: Length::Percentage(50.0), y: Length::Percentage(0.0) } &&
coords[1] == SpacePair { x: Length::Percentage(0.0), y: Length::Percentage(100.0) } &&
coords[2] == SpacePair { x: Length::Percentage(100.0), y: Length::Percentage(100.0) }
));
}
#[test]
fn test_parse_polygon_with_fill_rule() {
assert!(matches!(
BasicShape::from_str("polygon(evenodd, 50% 0%, 0% 100%, 100% 100%)"),
Ok(BasicShape::Polygon(PolygonShape {
fill_rule: Some(FillRule::EvenOdd),
coordinates: coords,
})) if coords.len() == 3
));
}
#[test]
fn test_parse_path() {
assert_eq!(
BasicShape::from_str("path('M 10 10 L 90 90')"),
Ok(BasicShape::Path(PathShape {
fill_rule: None,
path: "M 10 10 L 90 90".into(),
}))
);
}
#[test]
fn test_parse_path_with_fill_rule() {
assert_eq!(
BasicShape::from_str("path(evenodd, 'M 10 10 L 90 90')"),
Ok(BasicShape::Path(PathShape {
fill_rule: Some(FillRule::EvenOdd),
path: "M 10 10 L 90 90".into(),
}))
);
}
#[test]
fn test_parse_circle_percentage_radius() {
assert_eq!(
BasicShape::from_str("circle(50%)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::Length(Length::Percentage(50.0)),
radius_y: ShapeRadius::Length(Length::Percentage(50.0)),
position: ShapePosition::default(),
})))
);
}
#[test]
fn test_parse_circle_closest_side() {
assert_eq!(
BasicShape::from_str("circle(closest-side)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::ClosestSide,
radius_y: ShapeRadius::ClosestSide,
position: ShapePosition::default(),
})))
);
}
#[test]
fn test_parse_circle_farthest_side() {
assert_eq!(
BasicShape::from_str("circle(farthest-side)"),
Ok(BasicShape::Ellipse(Box::new(EllipseShape {
radius_x: ShapeRadius::FarthestSide,
radius_y: ShapeRadius::FarthestSide,
position: ShapePosition::default(),
})))
);
}
}