use tiny_skia::{BlendMode, LineCap, LineJoin, Stroke, Transform};
use super::color_space::RenderColorSpace;
use crate::fonts::graphics_state::TextState;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct RenderState {
pub ctm: Transform,
pub fill_color: tiny_skia::Color,
pub stroke_color: tiny_skia::Color,
pub fill_alpha: f32,
pub stroke_alpha: f32,
pub line_width: f64,
pub line_cap: u8,
pub line_join: u8,
pub miter_limit: f64,
pub dash_array: Vec<f32>,
pub dash_phase: f32,
pub text_state: TextState,
pub blend_mode: BlendMode,
pub fill_color_space: RenderColorSpace,
pub stroke_color_space: RenderColorSpace,
pub flatness: f64,
pub rendering_intent: &'static str,
pub soft_mask: Option<std::sync::Arc<tiny_skia::Mask>>,
pub fill_pattern: Option<std::sync::Arc<PatternData>>,
pub stroke_pattern: Option<std::sync::Arc<PatternData>>,
}
#[derive(Debug, Clone)]
pub(crate) struct PatternData {
pub pixmap: tiny_skia::Pixmap,
#[allow(dead_code)]
pub x_step: Option<f32>,
#[allow(dead_code)]
pub y_step: Option<f32>,
pub transform: Transform,
}
impl Default for RenderState {
fn default() -> Self {
Self {
ctm: Transform::identity(),
fill_color: tiny_skia::Color::BLACK,
stroke_color: tiny_skia::Color::BLACK,
fill_alpha: 1.0,
stroke_alpha: 1.0,
line_width: 1.0,
line_cap: 0,
line_join: 0,
miter_limit: 10.0,
dash_array: Vec::new(),
dash_phase: 0.0,
text_state: TextState::default(),
blend_mode: BlendMode::SourceOver,
fill_color_space: RenderColorSpace::DeviceGray,
stroke_color_space: RenderColorSpace::DeviceGray,
flatness: 0.0,
rendering_intent: "RelativeColorimetric",
soft_mask: None,
fill_pattern: None,
stroke_pattern: None,
}
}
}
impl RenderState {
pub(crate) fn to_stroke(&self) -> Stroke {
let mut stroke = Stroke {
width: self.line_width as f32,
line_cap: match self.line_cap {
1 => LineCap::Round,
2 => LineCap::Square,
_ => LineCap::Butt,
},
line_join: match self.line_join {
1 => LineJoin::Round,
2 => LineJoin::Bevel,
_ => LineJoin::Miter,
},
miter_limit: self.miter_limit as f32,
..Default::default()
};
if !self.dash_array.is_empty() {
stroke.dash = tiny_skia::StrokeDash::new(self.dash_array.clone(), self.dash_phase);
}
stroke
}
pub(crate) fn effective_fill_color(&self) -> tiny_skia::Color {
apply_alpha(self.fill_color, self.fill_alpha)
}
pub(crate) fn effective_stroke_color(&self) -> tiny_skia::Color {
apply_alpha(self.stroke_color, self.stroke_alpha)
}
}
fn apply_alpha(c: tiny_skia::Color, alpha: f32) -> tiny_skia::Color {
if alpha >= 1.0 {
return c;
}
tiny_skia::Color::from_rgba(c.red(), c.green(), c.blue(), c.alpha() * alpha).unwrap_or(c)
}
#[derive(Debug)]
pub(crate) struct RenderStateStack {
stack: Vec<RenderState>,
current: RenderState,
}
impl RenderStateStack {
pub fn new(base_transform: Transform) -> Self {
Self {
stack: Vec::new(),
current: RenderState {
ctm: base_transform,
..Default::default()
},
}
}
pub fn save(&mut self) {
self.stack.push(self.current.clone());
}
pub fn restore(&mut self) {
if let Some(prev) = self.stack.pop() {
self.current = prev;
}
}
pub fn state(&self) -> &RenderState {
&self.current
}
pub fn state_mut(&mut self) -> &mut RenderState {
&mut self.current
}
pub fn concat_ctm(&mut self, transform: Transform) {
self.current.ctm = self.current.ctm.pre_concat(transform);
}
#[cfg(test)]
pub fn depth(&self) -> usize {
self.stack.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pattern_data_carries_step_sizes_and_transform() {
let pixmap = tiny_skia::Pixmap::new(10, 20).unwrap();
let pat = PatternData {
pixmap,
x_step: Some(15.0),
y_step: Some(25.0),
transform: Transform::from_translate(5.0, 10.0),
};
assert_eq!(pat.x_step, Some(15.0));
assert_eq!(pat.y_step, Some(25.0));
assert_eq!(pat.transform.tx, 5.0);
assert_eq!(pat.transform.ty, 10.0);
}
#[test]
fn pattern_data_shading_has_no_step_sizes() {
let pixmap = tiny_skia::Pixmap::new(10, 10).unwrap();
let pat = PatternData {
pixmap,
x_step: None,
y_step: None,
transform: Transform::identity(),
};
assert!(pat.x_step.is_none());
assert!(pat.y_step.is_none());
}
#[test]
fn render_state_default_values() {
let state = RenderState::default();
assert_eq!(state.ctm, Transform::identity());
assert_eq!(state.fill_color, tiny_skia::Color::BLACK);
assert_eq!(state.stroke_color, tiny_skia::Color::BLACK);
assert_eq!(state.line_width, 1.0);
assert_eq!(state.line_cap, 0);
assert_eq!(state.line_join, 0);
assert_eq!(state.miter_limit, 10.0);
assert_eq!(state.fill_alpha, 1.0);
assert_eq!(state.stroke_alpha, 1.0);
}
#[test]
fn render_state_save_restore() {
let mut stack = RenderStateStack::new(Transform::identity());
stack.state_mut().line_width = 5.0;
stack.state_mut().fill_color = tiny_skia::Color::from_rgba8(255, 0, 0, 255);
stack.save();
stack.state_mut().line_width = 10.0;
stack.state_mut().fill_color = tiny_skia::Color::from_rgba8(0, 255, 0, 255);
assert_eq!(stack.state().line_width, 10.0);
stack.restore();
assert_eq!(stack.state().line_width, 5.0);
assert_eq!(
stack.state().fill_color,
tiny_skia::Color::from_rgba8(255, 0, 0, 255)
);
}
#[test]
fn render_state_cm_transform() {
let mut stack = RenderStateStack::new(Transform::identity());
let t = Transform::from_translate(100.0, 200.0);
stack.concat_ctm(t);
assert_eq!(stack.state().ctm.tx, 100.0);
assert_eq!(stack.state().ctm.ty, 200.0);
}
#[test]
fn render_state_nested_save_restore() {
let mut stack = RenderStateStack::new(Transform::identity());
stack.state_mut().line_width = 1.0;
stack.save();
stack.state_mut().line_width = 2.0;
stack.save();
stack.state_mut().line_width = 3.0;
assert_eq!(stack.depth(), 2);
assert_eq!(stack.state().line_width, 3.0);
stack.restore();
assert_eq!(stack.state().line_width, 2.0);
stack.restore();
assert_eq!(stack.state().line_width, 1.0);
}
#[test]
fn render_state_cm_concatenation() {
let mut stack = RenderStateStack::new(Transform::identity());
stack.concat_ctm(Transform::from_translate(50.0, 0.0));
stack.concat_ctm(Transform::from_translate(30.0, 0.0));
let eps = 0.001;
assert!((stack.state().ctm.tx - 80.0).abs() < eps);
}
#[test]
fn render_state_restore_underflow_ignored() {
let mut stack = RenderStateStack::new(Transform::identity());
stack.state_mut().line_width = 5.0;
stack.restore();
assert_eq!(stack.state().line_width, 5.0);
}
}