use svgtypes::{SimplePathSegment, SimplifyingPathParser};
use taffy::{Point, Size};
use tiny_skia::{
FillRule as TinyFillRule, LineCap as TinyLineCap, LineJoin as TinyLineJoin, Path as TinyPath,
PathBuilder as TinyPathBuilder, PathSegment as TinyPathSegment, Point as TinyPoint,
Rect as TinyRect, Stroke as TinyStroke, StrokeDash as TinyStrokeDash,
};
use crate::layout::style::Affine;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) enum Fill {
#[default]
NonZero,
EvenOdd,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) enum Join {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) enum Cap {
#[default]
Butt,
Round,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct DashPattern {
pub(crate) intervals: [f32; 2],
pub(crate) offset: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Stroke {
pub(crate) width: f32,
pub(crate) join: Join,
pub(crate) cap: Cap,
pub(crate) dash: Option<DashPattern>,
}
impl Stroke {
pub(crate) fn new(width: f32) -> Self {
Self {
width,
join: Join::Miter,
cap: Cap::Butt,
dash: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) enum Style {
#[default]
FillNonZero,
FillEvenOdd,
Stroke(Stroke),
}
impl From<Fill> for Style {
fn from(fill: Fill) -> Self {
match fill {
Fill::NonZero => Self::FillNonZero,
Fill::EvenOdd => Self::FillEvenOdd,
}
}
}
impl From<Stroke> for Style {
fn from(stroke: Stroke) -> Self {
Self::Stroke(stroke)
}
}
impl From<Fill> for TinyFillRule {
fn from(fill: Fill) -> Self {
match fill {
Fill::NonZero => TinyFillRule::Winding,
Fill::EvenOdd => TinyFillRule::EvenOdd,
}
}
}
impl From<Join> for TinyLineJoin {
fn from(join: Join) -> Self {
match join {
Join::Miter => TinyLineJoin::Miter,
Join::Round => TinyLineJoin::Round,
Join::Bevel => TinyLineJoin::Bevel,
}
}
}
impl From<Cap> for TinyLineCap {
fn from(cap: Cap) -> Self {
match cap {
Cap::Butt => TinyLineCap::Butt,
Cap::Round => TinyLineCap::Round,
}
}
}
impl From<Stroke> for TinyStroke {
fn from(stroke: Stroke) -> Self {
Self {
width: stroke.width,
line_cap: stroke.cap.into(),
line_join: stroke.join.into(),
dash: stroke
.dash
.and_then(|pattern| TinyStrokeDash::new(pattern.intervals.into(), pattern.offset)),
..TinyStroke::default()
}
}
}
impl Style {
pub(crate) fn fill_rule(self) -> TinyFillRule {
match self {
Style::FillEvenOdd => Fill::EvenOdd.into(),
Style::FillNonZero | Style::Stroke(_) => Fill::NonZero.into(),
}
}
pub(crate) fn stroke(self) -> Option<TinyStroke> {
match self {
Style::Stroke(stroke) => Some(stroke.into()),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) struct Placement {
pub(crate) left: i32,
pub(crate) top: i32,
pub(crate) width: u32,
pub(crate) height: u32,
}
impl Placement {
pub(crate) fn right(self) -> i32 {
self.left + self.width as i32
}
pub(crate) fn bottom(self) -> i32 {
self.top + self.height as i32
}
pub(crate) fn from_bounds(left: i32, top: i32, right: i32, bottom: i32) -> Option<Self> {
if left >= right || top >= bottom {
return None;
}
Some(Self {
left,
top,
width: (right - left) as u32,
height: (bottom - top) as u32,
})
}
pub(crate) fn translate(self, dx: i32, dy: i32) -> Self {
Self {
left: self.left + dx,
top: self.top + dy,
..self
}
}
pub(crate) fn inflate(self, padding: i32) -> Option<Self> {
Self::from_bounds(
self.left - padding,
self.top - padding,
self.right() + padding,
self.bottom() + padding,
)
}
pub(crate) fn clamp_to(self, size: Size<u32>) -> Option<Self> {
Self::from_bounds(
self.left.clamp(0, size.width as i32),
self.top.clamp(0, size.height as i32),
self.right().clamp(0, size.width as i32),
self.bottom().clamp(0, size.height as i32),
)
}
}
pub(crate) fn transformed_rect_extents(
origin: Point<f32>,
size: Size<f32>,
transform: Affine,
) -> Option<(f32, f32, f32, f32)> {
let corners = [
transform.transform_point(origin),
transform.transform_point(Point {
x: origin.x + size.width,
y: origin.y,
}),
transform.transform_point(Point {
x: origin.x,
y: origin.y + size.height,
}),
transform.transform_point(Point {
x: origin.x + size.width,
y: origin.y + size.height,
}),
];
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for point in corners {
min_x = min_x.min(point.x);
min_y = min_y.min(point.y);
max_x = max_x.max(point.x);
max_y = max_y.max(point.y);
}
if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() {
return None;
}
Some((min_x, min_y, max_x, max_y))
}
pub(crate) type Command = TinyPathSegment;
pub(crate) trait PathBuilder {
fn move_to(&mut self, point: (f32, f32));
fn line_to(&mut self, point: (f32, f32));
fn curve_to(&mut self, p1: (f32, f32), p2: (f32, f32), p3: (f32, f32));
fn close(&mut self);
fn add_ellipse(&mut self, center: (f32, f32), radius_x: f32, radius_y: f32);
}
impl PathBuilder for Vec<Command> {
fn move_to(&mut self, point: (f32, f32)) {
self.push(Command::MoveTo(TinyPoint::from_xy(point.0, point.1)));
}
fn line_to(&mut self, point: (f32, f32)) {
self.push(Command::LineTo(TinyPoint::from_xy(point.0, point.1)));
}
fn curve_to(&mut self, p1: (f32, f32), p2: (f32, f32), p3: (f32, f32)) {
self.push(Command::CubicTo(
TinyPoint::from_xy(p1.0, p1.1),
TinyPoint::from_xy(p2.0, p2.1),
TinyPoint::from_xy(p3.0, p3.1),
));
}
fn close(&mut self) {
self.push(Command::Close);
}
fn add_ellipse(&mut self, center: (f32, f32), radius_x: f32, radius_y: f32) {
let Some(rect) = TinyRect::from_ltrb(
center.0 - radius_x,
center.1 - radius_y,
center.0 + radius_x,
center.1 + radius_y,
) else {
return;
};
let mut builder = TinyPathBuilder::new();
builder.push_oval(rect);
if let Some(path) = builder.finish() {
self.extend(path.segments());
}
}
}
pub(crate) trait PathData {
fn commands(&self) -> Vec<Command>;
}
impl PathData for str {
fn commands(&self) -> Vec<Command> {
parse_svg_path_segments(self).unwrap_or_default()
}
}
pub(crate) fn build_path(commands: &[Command]) -> Option<TinyPath> {
let mut builder = TinyPathBuilder::new();
for command in commands {
match command {
Command::MoveTo(point) => builder.move_to(point.x, point.y),
Command::LineTo(point) => builder.line_to(point.x, point.y),
Command::QuadTo(p1, p2) => builder.quad_to(p1.x, p1.y, p2.x, p2.y),
Command::CubicTo(p1, p2, p3) => {
builder.cubic_to(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
Command::Close => builder.close(),
}
}
builder.finish()
}
fn parse_svg_path_segments(input: &str) -> Option<Vec<Command>> {
let mut commands = Vec::new();
for segment in SimplifyingPathParser::from(input) {
match segment.ok()? {
SimplePathSegment::MoveTo { x, y } => {
commands.push(Command::MoveTo(TinyPoint::from_xy(x as f32, y as f32)));
}
SimplePathSegment::LineTo { x, y } => {
commands.push(Command::LineTo(TinyPoint::from_xy(x as f32, y as f32)));
}
SimplePathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
commands.push(Command::CubicTo(
TinyPoint::from_xy(x1 as f32, y1 as f32),
TinyPoint::from_xy(x2 as f32, y2 as f32),
TinyPoint::from_xy(x as f32, y as f32),
));
}
SimplePathSegment::Quadratic { x1, y1, x, y } => {
commands.push(Command::QuadTo(
TinyPoint::from_xy(x1 as f32, y1 as f32),
TinyPoint::from_xy(x as f32, y as f32),
));
}
SimplePathSegment::ClosePath => {
commands.push(Command::Close);
}
}
}
Some(commands)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stroke_dash_is_forwarded_to_tiny_skia() {
let tiny: TinyStroke = Stroke {
width: 4.0,
join: Join::Miter,
cap: Cap::Round,
dash: Some(DashPattern {
intervals: [12.0, 8.0],
offset: 1.5,
}),
}
.into();
assert_eq!(tiny.width, 4.0);
assert_eq!(tiny.line_cap, TinyLineCap::Round);
assert!(tiny.dash.is_some());
}
}