skia-canvas 0.1.0

GPU-accelerated, multi-threaded HTML Canvas-compatible 2D rendering for Rust and Node, powered by Skia.
use skia_safe::utils::parse_path;
use skia_safe::{Path as SkPath, PathFillType};

use crate::native::error::NativeError;

/// Path winding rule. Matches SVG / Canvas semantics:
/// - `NonZero` (Skia's `Winding`) fills any region whose net winding is
///   non-zero.
/// - `EvenOdd` fills any region with an odd winding count.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FillRule {
    #[default]
    NonZero,
    EvenOdd,
}

impl FillRule {
    pub(crate) fn to_skia(self) -> PathFillType {
        match self {
            Self::NonZero => PathFillType::Winding,
            Self::EvenOdd => PathFillType::EvenOdd,
        }
    }
}

/// Vector path. Currently only constructible from SVG path data (the same
/// `d=""` syntax used by `<path>` elements and Studio's `ShapeItem.pathData`).
/// More constructors land alongside their use cases.
pub struct NativePath {
    pub(crate) inner: SkPath,
}

impl NativePath {
    pub fn from_svg(data: &str, fill_rule: FillRule) -> Result<Self, NativeError> {
        let mut path = parse_path::from_svg(data).ok_or_else(|| NativeError::InvalidSvgPath {
            reason: format!("could not parse SVG path data: {data:?}"),
        })?;
        path.set_fill_type(fill_rule.to_skia());
        Ok(Self { inner: path })
    }

    pub fn fill_rule(&self) -> FillRule {
        match self.inner.fill_type() {
            PathFillType::EvenOdd | PathFillType::InverseEvenOdd => FillRule::EvenOdd,
            _ => FillRule::NonZero,
        }
    }

    pub fn set_fill_rule(&mut self, fill_rule: FillRule) {
        self.inner.set_fill_type(fill_rule.to_skia());
    }
}

impl Clone for NativePath {
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
        }
    }
}

impl std::fmt::Debug for NativePath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NativePath")
            .field("fill_rule", &self.fill_rule())
            .finish()
    }
}