use super::{Rect, RenderError};
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum PointShape {
Circle,
Triangle,
Square,
Diamond,
Cross,
Plus,
}
impl PointShape {
pub const ALL: &[PointShape] = &[
PointShape::Circle,
PointShape::Triangle,
PointShape::Square,
PointShape::Diamond,
PointShape::Cross,
PointShape::Plus,
];
}
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum Linetype {
Solid,
Dashed,
Dotted,
DashDot,
LongDash,
TwoDash,
}
impl Linetype {
pub const ALL: &[Linetype] = &[
Linetype::Solid,
Linetype::Dashed,
Linetype::Dotted,
Linetype::DashDot,
Linetype::LongDash,
Linetype::TwoDash,
];
pub fn pattern(&self) -> &[(f64, f64)] {
match self {
Linetype::Solid => &[],
Linetype::Dashed => &[(6.0, 3.0)],
Linetype::Dotted => &[(2.0, 2.0)],
Linetype::DashDot => &[(6.0, 2.0), (2.0, 2.0)],
Linetype::LongDash => &[(10.0, 4.0)],
Linetype::TwoDash => &[(8.0, 3.0), (3.0, 3.0)],
}
}
}
#[derive(Clone, Debug)]
pub struct PointStyle {
pub color: (u8, u8, u8),
pub alpha: f64,
pub filled: bool,
pub shape: PointShape,
}
impl Default for PointStyle {
fn default() -> Self {
PointStyle {
color: (0, 0, 0),
alpha: 1.0,
filled: true,
shape: PointShape::Circle,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum LineEnd {
#[default]
Butt,
Round,
Square,
}
#[derive(Clone, Debug)]
pub struct LineStyle {
pub color: (u8, u8, u8),
pub alpha: f64,
pub width: f64,
pub linetype: Linetype,
pub lineend: LineEnd,
}
impl Default for LineStyle {
fn default() -> Self {
LineStyle {
color: (0, 0, 0),
alpha: 1.0,
width: 1.0,
linetype: Linetype::Solid,
lineend: LineEnd::Butt,
}
}
}
#[derive(Clone, Debug)]
pub struct RectStyle {
pub fill: Option<(u8, u8, u8)>,
pub stroke: Option<(u8, u8, u8)>,
pub stroke_width: f64,
pub alpha: f64,
pub clip: bool,
pub corner_radius: f64,
}
impl Default for RectStyle {
fn default() -> Self {
RectStyle {
fill: Some((128, 128, 128)),
stroke: None,
stroke_width: 1.0,
alpha: 1.0,
clip: true,
corner_radius: 0.0,
}
}
}
#[cfg(any(
feature = "canvas",
all(feature = "plotters", not(target_arch = "wasm32"))
))]
const ROUNDED_RECT_CORNER_SEGMENTS: usize = 8;
#[cfg(any(
feature = "canvas",
all(feature = "plotters", not(target_arch = "wasm32"))
))]
pub(crate) fn rounded_rect_points(
x0: f64,
y0: f64,
x1: f64,
y1: f64,
radius: f64,
) -> Vec<(f64, f64)> {
let (x0, x1) = (x0.min(x1), x0.max(x1));
let (y0, y1) = (y0.min(y1), y0.max(y1));
let r = radius.max(0.0).min((x1 - x0) / 2.0).min((y1 - y0) / 2.0);
if r <= 0.0 {
return vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)];
}
use std::f64::consts::{FRAC_PI_2, PI};
let n = ROUNDED_RECT_CORNER_SEGMENTS;
let corners = [
(x1 - r, y0 + r, -FRAC_PI_2, 0.0), (x1 - r, y1 - r, 0.0, FRAC_PI_2), (x0 + r, y1 - r, FRAC_PI_2, PI), (x0 + r, y0 + r, PI, 3.0 * FRAC_PI_2), ];
let mut pts = Vec::with_capacity(4 * (n + 1));
for (cx, cy, a0, a1) in corners {
for i in 0..=n {
let t = a0 + (a1 - a0) * (i as f64 / n as f64);
pts.push((cx + r * t.cos(), cy + r * t.sin()));
}
}
pts
}
#[cfg(all(
test,
any(
feature = "canvas",
all(feature = "plotters", not(target_arch = "wasm32"))
)
))]
mod rounded_rect_tests {
use super::rounded_rect_points;
#[test]
fn zero_radius_is_a_plain_rectangle() {
let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 0.0);
assert_eq!(
pts,
vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
);
}
#[test]
fn negative_radius_also_falls_back_to_a_plain_rectangle() {
let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, -5.0);
assert_eq!(
pts,
vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
);
}
#[test]
fn x_and_y_are_order_independent() {
let forward = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 3.0);
let swapped = rounded_rect_points(10.0, 20.0, 0.0, 0.0, 3.0);
assert_eq!(forward, swapped);
}
#[test]
fn radius_is_clamped_to_half_the_shorter_side() {
let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 1000.0);
for (x, y) in &pts {
assert!((0.0..=10.0).contains(x), "x={x} out of [0,10]");
assert!((0.0..=20.0).contains(y), "y={y} out of [0,20]");
}
}
#[test]
fn corners_are_inset_by_radius_from_the_rect_corners() {
let pts = rounded_rect_points(0.0, 0.0, 100.0, 50.0, 10.0);
assert!(pts.len() > 4);
for (x, y) in &pts {
assert!(*x >= 0.0 && *x <= 100.0);
assert!(*y >= 0.0 && *y <= 50.0);
}
}
}
#[derive(Clone, Debug)]
pub struct TextStyle {
pub color: (u8, u8, u8),
pub size: f64,
pub anchor: TextAnchor,
pub angle: f64,
pub family: Option<String>,
pub face: FontFace,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum FontFace {
#[default]
Plain,
Bold,
Italic,
}
impl Default for TextStyle {
fn default() -> Self {
TextStyle {
color: (50, 50, 50),
size: 12.0,
anchor: TextAnchor::Middle,
angle: 0.0,
family: None,
face: crate::render::backend::FontFace::Plain,
}
}
}
#[derive(Clone, Debug)]
pub enum TextAnchor {
Start,
Middle,
End,
}
pub trait DrawBackend {
fn draw_circle(
&mut self,
center: (f64, f64),
radius: f64,
style: &PointStyle,
) -> Result<(), RenderError>;
fn draw_line(&mut self, points: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError>;
fn draw_rect(
&mut self,
top_left: (f64, f64),
bottom_right: (f64, f64),
style: &RectStyle,
) -> Result<(), RenderError>;
fn draw_text(
&mut self,
text: &str,
pos: (f64, f64),
style: &TextStyle,
) -> Result<(), RenderError>;
fn draw_polygon(&mut self, points: &[(f64, f64)], style: &RectStyle)
-> Result<(), RenderError>;
fn draw_polygon_rings(
&mut self,
rings: &[Vec<(f64, f64)>],
style: &RectStyle,
) -> Result<(), RenderError> {
for (i, ring) in rings.iter().enumerate() {
let ring_style = if i == 0 {
style.clone()
} else {
RectStyle {
fill: None,
..style.clone()
}
};
self.draw_polygon(ring, &ring_style)?;
}
Ok(())
}
fn draw_text_with_title(
&mut self,
text: &str,
pos: (f64, f64),
style: &TextStyle,
_title: Option<&str>,
) -> Result<(), RenderError> {
self.draw_text(text, pos, style)
}
fn draw_text_runs(
&mut self,
runs: &[(&str, Option<&str>)],
pos: (f64, f64),
style: &TextStyle,
) -> Result<(), RenderError> {
let joined: String = runs.iter().map(|(t, _)| *t).collect();
self.draw_text(&joined, pos, style)
}
fn plot_area(&self) -> Rect;
fn total_area(&self) -> Rect;
fn set_tooltip(&mut self, _tooltip: Option<String>) {}
fn set_mark_axis(&mut self, _key: Option<String>) {}
fn set_mark_series(&mut self, _series: Option<String>) {}
fn set_mark_value(&mut self, _value: Option<String>) {}
fn set_mark_geom(&mut self, _geom: Option<&str>) {}
fn warn(&mut self, _message: String) {}
fn set_mark_part(&mut self, _part: Option<&str>) {}
fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
fn wants_formatted_values(&self) -> bool {
false
}
fn set_mark_fvalue(&mut self, _value: Option<String>) {}
fn set_mark_fx(&mut self, _value: Option<String>) {}
fn draw_shape(
&mut self,
center: (f64, f64),
radius: f64,
style: &PointStyle,
) -> Result<(), RenderError> {
match style.shape {
PointShape::Circle => self.draw_circle(center, radius, style),
_ => {
self.draw_circle(center, radius, style)
}
}
}
fn draw_quadratic(
&mut self,
p0: (f64, f64),
ctrl: (f64, f64),
p1: (f64, f64),
style: &LineStyle,
) -> Result<(), RenderError> {
if !(p0.0.is_finite()
&& p0.1.is_finite()
&& ctrl.0.is_finite()
&& ctrl.1.is_finite()
&& p1.0.is_finite()
&& p1.1.is_finite())
{
return Ok(());
}
const STEPS: usize = 32;
let mut pts = Vec::with_capacity(STEPS + 1);
for i in 0..=STEPS {
let t = i as f64 / STEPS as f64;
let one_minus_t = 1.0 - t;
let x =
one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
let y =
one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
pts.push((x, y));
}
self.draw_line(&pts, style)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{Rect, RenderError};
use std::cell::RefCell;
type RecordedPolygon = (Vec<(f64, f64)>, Option<(u8, u8, u8)>);
#[derive(Default)]
struct RecordingBackend {
lines: RefCell<Vec<Vec<(f64, f64)>>>,
circles: RefCell<Vec<((f64, f64), f64)>>,
polygons: RefCell<Vec<RecordedPolygon>>,
}
impl DrawBackend for RecordingBackend {
fn draw_circle(
&mut self,
center: (f64, f64),
radius: f64,
_style: &PointStyle,
) -> Result<(), RenderError> {
self.circles.borrow_mut().push((center, radius));
Ok(())
}
fn draw_line(
&mut self,
points: &[(f64, f64)],
_style: &LineStyle,
) -> Result<(), RenderError> {
self.lines.borrow_mut().push(points.to_vec());
Ok(())
}
fn draw_rect(
&mut self,
_top_left: (f64, f64),
_bottom_right: (f64, f64),
_style: &RectStyle,
) -> Result<(), RenderError> {
Ok(())
}
fn draw_text(
&mut self,
_text: &str,
_pos: (f64, f64),
_style: &TextStyle,
) -> Result<(), RenderError> {
Ok(())
}
fn draw_polygon(
&mut self,
points: &[(f64, f64)],
style: &RectStyle,
) -> Result<(), RenderError> {
self.polygons
.borrow_mut()
.push((points.to_vec(), style.fill));
Ok(())
}
fn plot_area(&self) -> Rect {
Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
}
}
fn total_area(&self) -> Rect {
self.plot_area()
}
}
#[test]
fn default_draw_quadratic_flattens_to_33_points() {
let mut b = RecordingBackend::default();
b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
.unwrap();
let lines = b.lines.borrow();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].len(), 33);
assert_eq!(lines[0][0], (0.0, 0.0));
assert_eq!(lines[0][32], (10.0, 0.0));
let mid = lines[0][16];
assert!((mid.0 - 5.0).abs() < 1e-9);
assert!((mid.1 - 5.0).abs() < 1e-9);
}
#[test]
fn default_draw_quadratic_drops_non_finite() {
let mut b = RecordingBackend::default();
b.draw_quadratic(
(f64::NAN, 0.0),
(5.0, 10.0),
(10.0, 0.0),
&LineStyle::default(),
)
.unwrap();
assert!(b.lines.borrow().is_empty());
}
#[test]
fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
let mut b = RecordingBackend::default();
for shape in PointShape::ALL {
b.draw_shape(
(1.0, 2.0),
3.0,
&PointStyle {
shape: *shape,
..Default::default()
},
)
.unwrap();
}
assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
for (center, radius) in b.circles.borrow().iter() {
assert_eq!(*center, (1.0, 2.0));
assert_eq!(*radius, 3.0);
}
}
#[test]
fn default_draw_polygon_rings_fills_only_the_exterior() {
let mut b = RecordingBackend::default();
let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
b.draw_polygon_rings(
&[exterior.clone(), hole.clone()],
&RectStyle {
fill: Some((200, 50, 50)),
stroke: Some((0, 0, 0)),
stroke_width: 1.0,
alpha: 1.0,
clip: true,
corner_radius: 0.0,
},
)
.unwrap();
let polys = b.polygons.borrow();
assert_eq!(polys.len(), 2, "one draw_polygon call per ring");
assert_eq!(polys[0], (exterior, Some((200, 50, 50))), "exterior filled");
assert_eq!(polys[1].0, hole);
assert_eq!(polys[1].1, None, "hole not filled (no real cut-out)");
}
#[test]
fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
let mut b = RecordingBackend::default();
let pa = b.plot_area();
let ta = b.total_area();
assert_eq!(
(pa.x, pa.y, pa.width, pa.height),
(ta.x, ta.y, ta.width, ta.height)
);
assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
.unwrap();
b.draw_text("label", (0.0, 0.0), &TextStyle::default())
.unwrap();
b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
.unwrap();
}
}