use crate::foundations::{Cast, Content, Smart, elem};
use crate::layout::{Abs, Corners, Length, Point, Rect, Rel, Sides, Size, Sizing};
use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
use kurbo::{PathEl, Shape as _};
#[elem(title = "Rectangle")]
pub struct RectElem {
pub width: Smart<Rel<Length>>,
pub height: Sizing,
pub fill: Option<Paint>,
#[fold]
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
#[positional]
pub body: Option<Content>,
}
#[elem]
pub struct SquareElem {
#[external]
pub size: Smart<Length>,
#[parse(
let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
match size {
None => args.named("width")?,
size => size,
}
)]
pub width: Smart<Rel<Length>>,
#[parse(match size {
None => args.named("height")?,
size => size.map(Into::into),
})]
pub height: Sizing,
pub fill: Option<Paint>,
#[fold]
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
#[positional]
pub body: Option<Content>,
}
#[elem]
pub struct EllipseElem {
pub width: Smart<Rel<Length>>,
pub height: Sizing,
pub fill: Option<Paint>,
#[fold]
pub stroke: Smart<Option<Stroke>>,
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
#[positional]
pub body: Option<Content>,
}
#[elem]
pub struct CircleElem {
#[external]
pub radius: Length,
#[parse(
let size = args
.named::<Smart<Length>>("radius")?
.map(|s| s.map(|r| 2.0 * Rel::from(r)));
match size {
None => args.named("width")?,
size => size,
}
)]
pub width: Smart<Rel<Length>>,
#[parse(match size {
None => args.named("height")?,
size => size.map(Into::into),
})]
pub height: Sizing,
pub fill: Option<Paint>,
#[fold]
#[default(Smart::Auto)]
pub stroke: Smart<Option<Stroke>>,
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
#[positional]
pub body: Option<Content>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Shape {
pub geometry: Geometry,
pub fill: Option<Paint>,
pub fill_rule: FillRule,
pub stroke: Option<FixedStroke>,
}
impl Shape {
pub fn bbox(&self, include_stroke: bool) -> Rect {
self.geometry
.bbox(if include_stroke { self.stroke.as_ref() } else { None })
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum FillRule {
#[default]
NonZero,
EvenOdd,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Geometry {
Line(Point),
Rect(Size),
Curve(Curve),
}
impl Geometry {
pub fn filled(self, fill: impl Into<Paint>) -> Shape {
Shape {
geometry: self,
fill: Some(fill.into()),
fill_rule: FillRule::default(),
stroke: None,
}
}
pub fn stroked(self, stroke: FixedStroke) -> Shape {
Shape {
geometry: self,
fill: None,
fill_rule: FillRule::default(),
stroke: Some(stroke),
}
}
pub fn filled_and_stroked(
self,
fill: impl Into<Paint>,
stroke: FixedStroke,
) -> Shape {
Shape {
geometry: self,
fill: Some(fill.into()),
fill_rule: FillRule::default(),
stroke: Some(stroke),
}
}
pub fn bbox(&self, stroke: Option<&FixedStroke>) -> Rect {
match self {
Self::Line(end) => {
if let Some(stroke) = stroke {
bbox_of_stroked_line(end, stroke)
} else {
Rect::new(end.min(Point::zero()), end.max(Point::zero()))
}
}
Self::Rect(size) => {
let min = size.to_point().min(Point::zero());
let stroke_width = stroke.map(|s| s.thickness).unwrap_or(Abs::zero());
Rect::from_pos_size(
min.map(|i| i - 0.5 * stroke_width),
size.map(|i| i.abs() + stroke_width),
)
}
Self::Curve(curve) => curve.bbox(stroke),
}
}
}
fn bbox_of_stroked_line(end: &Point, stroke: &FixedStroke) -> Rect {
let cap = match stroke.cap {
super::LineCap::Butt => kurbo::Cap::Butt,
super::LineCap::Round => kurbo::Cap::Round,
super::LineCap::Square => kurbo::Cap::Square,
};
let style = kurbo::Stroke::new(stroke.thickness.to_raw()).with_caps(cap);
let opts = kurbo::StrokeOpts::default();
let tolerance = 0.01;
let bbox = kurbo::stroke(
[PathEl::LineTo(kurbo::Point::new(end.x.to_raw(), end.y.to_raw()))],
&style,
&opts,
tolerance,
)
.bounding_box();
Rect::new(
Point::new(Abs::raw(bbox.x0), Abs::raw(bbox.y0)),
Point::new(Abs::raw(bbox.x1), Abs::raw(bbox.y1)),
)
}