use kurbo::{Affine, BezPath, Point, Rect};
use peniko::{Brush, Color, ImageData};
use crate::glyph::GlyphRun;
use crate::shader::ShaderProgram;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct CornerRadii {
pub top_left: f64,
pub top_right: f64,
pub bottom_right: f64,
pub bottom_left: f64,
}
impl CornerRadii {
pub const fn new(top_left: f64, top_right: f64, bottom_right: f64, bottom_left: f64) -> Self {
Self {
top_left,
top_right,
bottom_right,
bottom_left,
}
}
pub const fn uniform(radius: f64) -> Self {
Self::new(radius, radius, radius, radius)
}
pub fn largest(&self) -> f64 {
self.top_left
.max(self.top_right)
.max(self.bottom_right)
.max(self.bottom_left)
}
}
impl From<f64> for CornerRadii {
fn from(radius: f64) -> Self {
Self::uniform(radius)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DashPattern {
pub on: f64,
pub off: f64,
pub phase: f64,
}
const DASH_PERIOD_EPSILON: f64 = 0.1;
impl DashPattern {
pub const fn new(on: f64, off: f64) -> Self {
Self {
on,
off,
phase: 0.0,
}
}
pub const fn with_phase(mut self, phase: f64) -> Self {
self.phase = phase;
self
}
pub fn is_effective(&self) -> bool {
self.on.is_finite()
&& self.off.is_finite()
&& self.phase.is_finite()
&& self.on > 0.0
&& self.off > 0.0
&& (self.on + self.off) >= DASH_PERIOD_EPSILON
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathStyle {
Fill,
Stroke {
width: f64,
dash: Option<DashPattern>,
},
}
#[derive(Clone, Debug)]
pub enum Command {
FillRect {
rect: Rect,
brush: Brush,
transform: Affine,
},
RoundedRect {
rect: Rect,
radii: CornerRadii,
brush: Brush,
transform: Affine,
},
Line {
p0: Point,
p1: Point,
width: f64,
brush: Brush,
transform: Affine,
},
GlyphRun(GlyphRun),
PushClip { rect: Rect, transform: Affine },
PushClipRounded {
rect: Rect,
radii: CornerRadii,
transform: Affine,
},
PopClip,
Image {
data: ImageData,
dest: Rect,
transform: Affine,
},
BlurredRoundedRect {
rect: Rect,
radii: CornerRadii,
std_dev: f64,
color: Color,
transform: Affine,
},
PushLayer {
rect: Rect,
alpha: f32,
transform: Affine,
},
PopLayer,
ClearRect { rect: Rect, transform: Affine },
Path {
path: BezPath,
style: PathStyle,
brush: Brush,
transform: Affine,
},
ShaderQuad {
program: ShaderProgram,
dest: Rect,
transform: Affine,
time: f32,
},
PushSnapshot {
key: u64,
rect: Rect,
alpha: f32,
scale: f64,
transform: Affine,
},
PopSnapshot,
SceneTexture {
id: u64,
dest: Rect,
transform: Affine,
},
}
#[derive(Clone, Debug, Default)]
pub struct Scene {
commands: Vec<Command>,
pub(crate) transform_stack: Vec<Affine>,
pub(crate) snapshot_depth: usize,
}
impl Scene {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.commands.clear();
self.snapshot_depth = 0;
}
pub fn commands(&self) -> &[Command] {
&self.commands
}
pub(crate) fn push(&mut self, command: Command) {
self.commands.push(command);
}
#[doc(hidden)]
pub fn snapshot_depth(&self) -> usize {
self.snapshot_depth
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_scene_has_no_commands() {
let scene = Scene::new();
assert!(scene.commands().is_empty());
}
#[test]
fn reset_clears_commands() {
let mut scene = Scene::new();
scene.push(Command::PushClip {
rect: Rect::new(0.0, 0.0, 1.0, 1.0),
transform: Affine::IDENTITY,
});
assert_eq!(scene.commands().len(), 1);
scene.reset();
assert!(scene.commands().is_empty());
}
#[test]
fn reset_clears_snapshot_depth() {
let mut scene = Scene::new();
scene.snapshot_depth = 2;
scene.reset();
assert_eq!(scene.snapshot_depth, 0);
}
#[test]
fn corner_radii_from_f64_is_uniform() {
assert_eq!(CornerRadii::from(6.0), CornerRadii::new(6.0, 6.0, 6.0, 6.0));
assert_eq!(CornerRadii::from(6.0), CornerRadii::uniform(6.0));
}
#[test]
fn corner_radii_new_orders_corners_clockwise_from_top_left() {
let radii = CornerRadii::new(1.0, 2.0, 3.0, 4.0);
assert_eq!(radii.top_left, 1.0);
assert_eq!(radii.top_right, 2.0);
assert_eq!(radii.bottom_right, 3.0);
assert_eq!(radii.bottom_left, 4.0);
}
#[test]
fn corner_radii_largest_picks_the_biggest_corner() {
assert_eq!(CornerRadii::new(1.0, 9.0, 3.0, 4.0).largest(), 9.0);
assert_eq!(CornerRadii::uniform(2.0).largest(), 2.0);
assert_eq!(CornerRadii::default().largest(), 0.0);
}
#[test]
fn dash_pattern_defaults_to_zero_phase_and_carries_with_phase() {
let dash = DashPattern::new(4.0, 2.0);
assert_eq!(dash.phase, 0.0);
assert_eq!(dash.with_phase(1.5).phase, 1.5);
assert_eq!(dash.with_phase(1.5).on, 4.0);
assert_eq!(dash.with_phase(1.5).off, 2.0);
}
#[test]
fn degenerate_dash_patterns_are_not_effective() {
assert!(DashPattern::new(4.0, 2.0).is_effective());
assert!(!DashPattern::new(0.0, 2.0).is_effective());
assert!(!DashPattern::new(4.0, 0.0).is_effective());
assert!(!DashPattern::new(-4.0, 2.0).is_effective());
assert!(!DashPattern::new(f64::NAN, 2.0).is_effective());
assert!(!DashPattern::new(f64::INFINITY, 2.0).is_effective());
assert!(
!DashPattern::new(4.0, 2.0)
.with_phase(f64::NAN)
.is_effective()
);
}
#[test]
fn tiny_period_dash_patterns_are_not_effective() {
assert!(!DashPattern::new(1e-9, 1e-9).is_effective());
assert!(!DashPattern::new(0.01, 0.01).is_effective()); assert!(!DashPattern::new(0.04, 0.05).is_effective()); }
#[test]
fn dash_pattern_at_epsilon_boundary_is_effective() {
assert!(DashPattern::new(0.05, 0.05).is_effective()); assert!(DashPattern::new(0.03, 0.07).is_effective()); }
#[test]
fn normal_dash_patterns_remain_effective() {
assert!(DashPattern::new(1.0, 0.5).is_effective());
assert!(DashPattern::new(4.0, 2.0).is_effective());
assert!(DashPattern::new(10.0, 10.0).is_effective());
}
}