pub use crate::core::{Affine2D, Bbox, Path, color::Rgba};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapStyle {
#[default]
Butt,
Round,
Projecting,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JoinStyle {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Debug, Clone)]
pub struct GraphicsContext {
pub line_width: f64,
pub dashes: Option<(f64, Vec<f64>)>,
pub cap: CapStyle,
pub join: JoinStyle,
pub alpha: Option<f64>,
pub antialiased: bool,
pub clip_rect: Option<Bbox>,
pub clip_path: Option<Path>,
pub stroke: Option<Rgba>,
}
impl GraphicsContext {
#[must_use]
pub fn new() -> Self {
Self {
line_width: 1.0,
dashes: None,
cap: CapStyle::default(),
join: JoinStyle::default(),
alpha: None,
antialiased: true,
clip_rect: None,
clip_path: None,
stroke: None,
}
}
#[must_use]
pub fn with_line_width(mut self, line_width: f64) -> Self {
self.line_width = line_width;
self
}
#[must_use]
pub fn with_stroke(mut self, stroke: Rgba) -> Self {
self.stroke = Some(stroke);
self
}
#[must_use]
pub fn with_cap(mut self, cap: CapStyle) -> Self {
self.cap = cap;
self
}
#[must_use]
pub fn with_join(mut self, join: JoinStyle) -> Self {
self.join = join;
self
}
#[must_use]
pub fn with_alpha(mut self, alpha: f64) -> Self {
self.alpha = Some(alpha);
self
}
}
impl Default for GraphicsContext {
fn default() -> Self {
Self::new()
}
}
pub trait Renderer {
fn draw_path(
&mut self,
gc: &GraphicsContext,
path: &Path,
transform: &Affine2D,
fill: Option<Rgba>,
);
fn draw_markers(
&mut self,
gc: &GraphicsContext,
marker_path: &Path,
marker_transform: &Affine2D,
path: &Path,
transform: &Affine2D,
fill: Option<Rgba>,
) {
for &[vx, vy] in path.vertices() {
let (dx, dy) = transform.transform_point((vx, vy));
let placed = marker_transform.then(&Affine2D::from_translation(dx, dy));
self.draw_path(gc, marker_path, &placed, fill);
}
}
fn draw_image(
&mut self,
gc: &GraphicsContext,
x: f64,
y: f64,
rgba: &[u8],
width: usize,
height: usize,
) {
let _ = (gc, x, y, rgba, width, height);
}
#[allow(clippy::too_many_arguments)]
fn draw_text(
&mut self,
gc: &GraphicsContext,
x: f64,
y: f64,
text: &str,
font_size_px: f64,
angle_deg: f64,
color: Rgba,
) {
let _ = (gc, x, y, text, font_size_px, angle_deg, color);
}
fn flipy(&self) -> bool {
true
}
fn points_to_pixels(&self, points: f64) -> f64 {
points
}
fn decoration_scale(&self) -> f64 {
1.0
}
fn canvas_size(&self) -> (f64, f64);
}
pub(crate) struct ClippedRenderer<'a> {
inner: &'a mut dyn Renderer,
rect: Bbox,
}
impl<'a> ClippedRenderer<'a> {
pub(crate) fn new(inner: &'a mut dyn Renderer, rect: Bbox) -> Self {
Self { inner, rect }
}
fn clipped(&self, gc: &GraphicsContext) -> Option<GraphicsContext> {
let clip = match gc.clip_rect {
Some(existing) => existing.intersection(&self.rect)?,
None => self.rect,
};
if clip.width() <= 0.0 || clip.height() <= 0.0 {
return None;
}
Some(GraphicsContext {
clip_rect: Some(clip),
..gc.clone()
})
}
}
impl Renderer for ClippedRenderer<'_> {
fn draw_path(
&mut self,
gc: &GraphicsContext,
path: &Path,
transform: &Affine2D,
fill: Option<Rgba>,
) {
if let Some(gc) = self.clipped(gc) {
self.inner.draw_path(&gc, path, transform, fill);
}
}
fn draw_markers(
&mut self,
gc: &GraphicsContext,
marker_path: &Path,
marker_transform: &Affine2D,
path: &Path,
transform: &Affine2D,
fill: Option<Rgba>,
) {
if let Some(gc) = self.clipped(gc) {
self.inner
.draw_markers(&gc, marker_path, marker_transform, path, transform, fill);
}
}
fn draw_image(
&mut self,
gc: &GraphicsContext,
x: f64,
y: f64,
rgba: &[u8],
width: usize,
height: usize,
) {
if let Some(gc) = self.clipped(gc) {
self.inner.draw_image(&gc, x, y, rgba, width, height);
}
}
fn draw_text(
&mut self,
gc: &GraphicsContext,
x: f64,
y: f64,
text: &str,
font_size_px: f64,
angle_deg: f64,
color: Rgba,
) {
if let Some(gc) = self.clipped(gc) {
self.inner
.draw_text(&gc, x, y, text, font_size_px, angle_deg, color);
}
}
fn flipy(&self) -> bool {
self.inner.flipy()
}
fn points_to_pixels(&self, points: f64) -> f64 {
self.inner.points_to_pixels(points)
}
fn decoration_scale(&self) -> f64 {
self.inner.decoration_scale()
}
fn canvas_size(&self) -> (f64, f64) {
self.inner.canvas_size()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct RecordingRenderer {
calls: Vec<(f64, f64)>,
}
impl Renderer for RecordingRenderer {
fn draw_path(
&mut self,
_gc: &GraphicsContext,
_path: &Path,
transform: &Affine2D,
_fill: Option<Rgba>,
) {
let [.., e, f] = transform.matrix();
self.calls.push((e, f));
}
fn canvas_size(&self) -> (f64, f64) {
(640.0, 480.0)
}
}
#[test]
fn default_context_matches_documented_defaults() {
let gc = GraphicsContext::default();
assert_eq!(gc.line_width, 1.0);
assert!(gc.antialiased);
assert_eq!(gc.cap, CapStyle::Butt);
assert_eq!(gc.join, JoinStyle::Miter);
assert!(gc.dashes.is_none());
assert!(gc.alpha.is_none());
assert!(gc.clip_rect.is_none());
assert!(gc.clip_path.is_none());
assert!(gc.stroke.is_none());
}
#[test]
fn builder_setters() {
let red = Rgba::RED;
let gc = GraphicsContext::new()
.with_line_width(2.5)
.with_stroke(red)
.with_cap(CapStyle::Round)
.with_join(JoinStyle::Bevel)
.with_alpha(0.5);
assert_eq!(gc.line_width, 2.5);
assert_eq!(gc.stroke, Some(red));
assert_eq!(gc.cap, CapStyle::Round);
assert_eq!(gc.join, JoinStyle::Bevel);
assert_eq!(gc.alpha, Some(0.5));
}
#[test]
fn draw_markers_fans_out_one_draw_path_per_point() {
let mut renderer = RecordingRenderer::default();
let gc = GraphicsContext::default();
let marker = Path::unit_rectangle();
let points = Path::from_polyline(&[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]);
renderer.draw_markers(
&gc,
&marker,
&Affine2D::identity(),
&points,
&Affine2D::identity(),
Some(Rgba::BLUE),
);
assert_eq!(renderer.calls, vec![(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]);
}
#[test]
fn draw_markers_honors_marker_transform_then_translation() {
let mut renderer = RecordingRenderer::default();
let gc = GraphicsContext::default();
let marker = Path::unit_rectangle();
let points = Path::from_polyline(&[[10.0, 20.0]]);
renderer.draw_markers(
&gc,
&marker,
&Affine2D::from_translation(100.0, 200.0),
&points,
&Affine2D::from_scale(2.0, 2.0),
None,
);
assert_eq!(renderer.calls, vec![(120.0, 240.0)]);
}
#[test]
fn default_capabilities() {
let renderer = RecordingRenderer::default();
assert!(renderer.flipy());
assert_eq!(renderer.points_to_pixels(12.0), 12.0);
assert_eq!(renderer.canvas_size(), (640.0, 480.0));
}
}