takumi 1.7.0

Render UI component trees to images.
Documentation
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());
  }
}