ggplot_rs/render/backend.rs
1use super::{Rect, RenderError};
2
3/// Point shape types.
4#[derive(Clone, Debug, Copy, PartialEq, Eq)]
5pub enum PointShape {
6 Circle,
7 Triangle,
8 Square,
9 Diamond,
10 Cross,
11 Plus,
12}
13
14impl PointShape {
15 /// All shapes in order (for discrete scale mapping).
16 pub const ALL: &[PointShape] = &[
17 PointShape::Circle,
18 PointShape::Triangle,
19 PointShape::Square,
20 PointShape::Diamond,
21 PointShape::Cross,
22 PointShape::Plus,
23 ];
24}
25
26/// Line type patterns.
27#[derive(Clone, Debug, Copy, PartialEq, Eq)]
28pub enum Linetype {
29 Solid,
30 Dashed,
31 Dotted,
32 DashDot,
33 LongDash,
34 TwoDash,
35}
36
37impl Linetype {
38 /// All linetypes in order (for discrete scale mapping).
39 pub const ALL: &[Linetype] = &[
40 Linetype::Solid,
41 Linetype::Dashed,
42 Linetype::Dotted,
43 Linetype::DashDot,
44 Linetype::LongDash,
45 Linetype::TwoDash,
46 ];
47
48 /// Get dash pattern as (draw_len, gap_len) pairs in pixels.
49 pub fn pattern(&self) -> &[(f64, f64)] {
50 match self {
51 Linetype::Solid => &[],
52 Linetype::Dashed => &[(6.0, 3.0)],
53 Linetype::Dotted => &[(2.0, 2.0)],
54 Linetype::DashDot => &[(6.0, 2.0), (2.0, 2.0)],
55 Linetype::LongDash => &[(10.0, 4.0)],
56 Linetype::TwoDash => &[(8.0, 3.0), (3.0, 3.0)],
57 }
58 }
59}
60
61/// Style for drawing points/circles.
62#[derive(Clone, Debug)]
63pub struct PointStyle {
64 pub color: (u8, u8, u8),
65 pub alpha: f64,
66 pub filled: bool,
67 pub shape: PointShape,
68}
69
70impl Default for PointStyle {
71 fn default() -> Self {
72 PointStyle {
73 color: (0, 0, 0),
74 alpha: 1.0,
75 filled: true,
76 shape: PointShape::Circle,
77 }
78 }
79}
80
81/// Line cap style (R grid's `lineend`), applied to the stroked ends of a
82/// polyline/path. `Butt` is SVG's own default, so it is written on the wire
83/// only when it differs (byte-identical default output).
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
85pub enum LineEnd {
86 #[default]
87 Butt,
88 Round,
89 Square,
90}
91
92/// Style for drawing lines.
93#[derive(Clone, Debug)]
94pub struct LineStyle {
95 pub color: (u8, u8, u8),
96 pub alpha: f64,
97 pub width: f64,
98 pub linetype: Linetype,
99 pub lineend: LineEnd,
100}
101
102impl Default for LineStyle {
103 fn default() -> Self {
104 LineStyle {
105 color: (0, 0, 0),
106 alpha: 1.0,
107 width: 1.0,
108 linetype: Linetype::Solid,
109 lineend: LineEnd::Butt,
110 }
111 }
112}
113
114/// Style for drawing rectangles/polygons.
115#[derive(Clone, Debug)]
116pub struct RectStyle {
117 pub fill: Option<(u8, u8, u8)>,
118 pub stroke: Option<(u8, u8, u8)>,
119 pub stroke_width: f64,
120 pub alpha: f64,
121 /// Whether to clip this rect to the plot area. Default `true` for data elements.
122 /// Set to `false` for non-data elements (backgrounds, strips, legends).
123 pub clip: bool,
124 /// Corner radius in px, for a rounded rectangle (ggplot2 `geom_label(label.r)`,
125 /// ggchicklet `radius`). `0.0` (the default) draws a plain rectangle; backends
126 /// that cannot draw rounded corners natively fall back to a plain rectangle.
127 pub corner_radius: f64,
128}
129
130impl Default for RectStyle {
131 fn default() -> Self {
132 RectStyle {
133 fill: Some((128, 128, 128)),
134 stroke: None,
135 stroke_width: 1.0,
136 alpha: 1.0,
137 clip: true,
138 corner_radius: 0.0,
139 }
140 }
141}
142
143/// Number of line segments approximating each 90° corner of a rounded rect in
144/// backends with no native rounded-rect primitive (pixel, plotters). Higher
145/// is smoother; 8 is visually indistinguishable from a true arc at the sizes
146/// these rects are drawn at (node/label boxes, a few tens to ~200px).
147///
148/// Only [`pixel_backend`](super::pixel_backend) (feature `canvas`) and
149/// [`plotters_backend`](super::plotters_backend) (feature `plotters`) call
150/// [`rounded_rect_points`] — `svg_backend` emits a native `rx`/`ry` instead —
151/// so both it and the function are cfg-gated the same way those backends
152/// are, rather than left for `--no-default-features` to flag as dead code.
153#[cfg(any(
154 feature = "canvas",
155 all(feature = "plotters", not(target_arch = "wasm32"))
156))]
157const ROUNDED_RECT_CORNER_SEGMENTS: usize = 8;
158
159/// A closed polygon approximating a rectangle with rounded corners, for
160/// backends that must rasterize/triangulate rather than emit a native rounded
161/// rect (see [`DrawBackend::draw_rect`]'s `corner_radius`, via [`RectStyle`]).
162///
163/// `radius` is clamped to half the shorter side, so an oversized radius draws
164/// a "stadium" (fully rounded) shape rather than overshooting. A
165/// non-positive radius returns the plain 4-corner rectangle.
166///
167/// Corners go clockwise in screen coordinates (y down), starting at the
168/// top-right, so the first and last points sit either side of the top-right
169/// corner — callers that stroke the result as a closed loop (last point back
170/// to the first) get a seamless boundary.
171#[cfg(any(
172 feature = "canvas",
173 all(feature = "plotters", not(target_arch = "wasm32"))
174))]
175pub(crate) fn rounded_rect_points(
176 x0: f64,
177 y0: f64,
178 x1: f64,
179 y1: f64,
180 radius: f64,
181) -> Vec<(f64, f64)> {
182 let (x0, x1) = (x0.min(x1), x0.max(x1));
183 let (y0, y1) = (y0.min(y1), y0.max(y1));
184 let r = radius.max(0.0).min((x1 - x0) / 2.0).min((y1 - y0) / 2.0);
185 if r <= 0.0 {
186 return vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)];
187 }
188 use std::f64::consts::{FRAC_PI_2, PI};
189 let n = ROUNDED_RECT_CORNER_SEGMENTS;
190 // (corner centre, start angle, end angle), clockwise, starting top-right.
191 let corners = [
192 (x1 - r, y0 + r, -FRAC_PI_2, 0.0), // top-right
193 (x1 - r, y1 - r, 0.0, FRAC_PI_2), // bottom-right
194 (x0 + r, y1 - r, FRAC_PI_2, PI), // bottom-left
195 (x0 + r, y0 + r, PI, 3.0 * FRAC_PI_2), // top-left
196 ];
197 let mut pts = Vec::with_capacity(4 * (n + 1));
198 for (cx, cy, a0, a1) in corners {
199 for i in 0..=n {
200 let t = a0 + (a1 - a0) * (i as f64 / n as f64);
201 pts.push((cx + r * t.cos(), cy + r * t.sin()));
202 }
203 }
204 pts
205}
206
207#[cfg(all(
208 test,
209 any(
210 feature = "canvas",
211 all(feature = "plotters", not(target_arch = "wasm32"))
212 )
213))]
214mod rounded_rect_tests {
215 use super::rounded_rect_points;
216
217 #[test]
218 fn zero_radius_is_a_plain_rectangle() {
219 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 0.0);
220 assert_eq!(
221 pts,
222 vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
223 );
224 }
225
226 #[test]
227 fn negative_radius_also_falls_back_to_a_plain_rectangle() {
228 // `radius.max(0.0)` guards a caller passing a negative radius
229 // (e.g. a miscomputed value) — it must not panic or produce a
230 // garbage polygon, just the same plain-rect fallback as 0.0.
231 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, -5.0);
232 assert_eq!(
233 pts,
234 vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
235 );
236 }
237
238 #[test]
239 fn x_and_y_are_order_independent() {
240 // Callers may pass (x1, y1) before (x0, y0) (e.g. a box whose
241 // ymax < ymin before min/max normalisation elsewhere) — the
242 // function itself must normalise, not assume sorted input.
243 let forward = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 3.0);
244 let swapped = rounded_rect_points(10.0, 20.0, 0.0, 0.0, 3.0);
245 assert_eq!(forward, swapped);
246 }
247
248 #[test]
249 fn radius_is_clamped_to_half_the_shorter_side() {
250 // A radius bigger than the box collapses to a "stadium": every point
251 // must stay within the rect's bounds.
252 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 1000.0);
253 for (x, y) in &pts {
254 assert!((0.0..=10.0).contains(x), "x={x} out of [0,10]");
255 assert!((0.0..=20.0).contains(y), "y={y} out of [0,20]");
256 }
257 }
258
259 #[test]
260 fn corners_are_inset_by_radius_from_the_rect_corners() {
261 let pts = rounded_rect_points(0.0, 0.0, 100.0, 50.0, 10.0);
262 // The arc midpoint of the top-right corner sits at 45° in from the
263 // corner, inset by r*(1 - cos(45°)) from the straight edges.
264 assert!(pts.len() > 4);
265 for (x, y) in &pts {
266 assert!(*x >= 0.0 && *x <= 100.0);
267 assert!(*y >= 0.0 && *y <= 50.0);
268 }
269 }
270}
271
272/// Style for drawing text.
273#[derive(Clone, Debug)]
274pub struct TextStyle {
275 pub color: (u8, u8, u8),
276 pub size: f64,
277 pub anchor: TextAnchor,
278 pub angle: f64,
279 /// Font family (e.g., "serif", "monospace"). None defaults to "sans-serif".
280 pub family: Option<String>,
281 /// Font face (R's `element_text(face = ...)`).
282 pub face: FontFace,
283}
284
285/// Font face / weight (R's `element_text(face = ...)`).
286#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
287pub enum FontFace {
288 #[default]
289 Plain,
290 Bold,
291 Italic,
292}
293
294impl Default for TextStyle {
295 fn default() -> Self {
296 TextStyle {
297 color: (50, 50, 50),
298 size: 12.0,
299 anchor: TextAnchor::Middle,
300 angle: 0.0,
301 family: None,
302 face: crate::render::backend::FontFace::Plain,
303 }
304 }
305}
306
307#[derive(Clone, Debug)]
308pub enum TextAnchor {
309 Start,
310 Middle,
311 End,
312}
313
314/// Our rendering abstraction, independent of plotters details.
315pub trait DrawBackend {
316 fn draw_circle(
317 &mut self,
318 center: (f64, f64),
319 radius: f64,
320 style: &PointStyle,
321 ) -> Result<(), RenderError>;
322 fn draw_line(&mut self, points: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError>;
323 fn draw_rect(
324 &mut self,
325 top_left: (f64, f64),
326 bottom_right: (f64, f64),
327 style: &RectStyle,
328 ) -> Result<(), RenderError>;
329 fn draw_text(
330 &mut self,
331 text: &str,
332 pos: (f64, f64),
333 style: &TextStyle,
334 ) -> Result<(), RenderError>;
335 fn draw_polygon(&mut self, points: &[(f64, f64)], style: &RectStyle)
336 -> Result<(), RenderError>;
337
338 /// Draw a polygon with holes: `rings[0]` is the exterior, every other
339 /// ring a hole cut out of it under the even-odd fill rule (`geom_sf`'s
340 /// `Polygon`, OGC/`sf` convention — see
341 /// [`spatial::Geometry::Polygon`](crate::spatial::Geometry::Polygon)).
342 /// One `stroke`/`stroke_width` draws every ring's outline, exterior and
343 /// holes alike (as `ggplot2`'s `geom_sf` does).
344 ///
345 /// Default: no real hole cut-out (for backends that pre-date this
346 /// method) — the exterior ring filled and stroked, every other ring
347 /// stroked only. Backends with a resolution-independent fill (SVG) or a
348 /// scanline rasteriser (pixel/plotters) should override this to use
349 /// their fill rule across every ring at once.
350 fn draw_polygon_rings(
351 &mut self,
352 rings: &[Vec<(f64, f64)>],
353 style: &RectStyle,
354 ) -> Result<(), RenderError> {
355 for (i, ring) in rings.iter().enumerate() {
356 let ring_style = if i == 0 {
357 style.clone()
358 } else {
359 RectStyle {
360 fill: None,
361 ..style.clone()
362 }
363 };
364 self.draw_polygon(ring, &ring_style)?;
365 }
366 Ok(())
367 }
368
369 /// Like [`draw_text`](Self::draw_text), but with an optional `<title>`
370 /// tooltip carrying the *full* text when `text` itself has been
371 /// shortened (e.g. a truncated-with-ellipsis legend label, #159).
372 /// Default: ignores `title` and draws `text` plain — backends without
373 /// native tooltip support (anything but SVG) have no better option.
374 /// SVG overrides this to emit the `<title>` child, escaped the same
375 /// way every other text run is.
376 fn draw_text_with_title(
377 &mut self,
378 text: &str,
379 pos: (f64, f64),
380 style: &TextStyle,
381 _title: Option<&str>,
382 ) -> Result<(), RenderError> {
383 self.draw_text(text, pos, style)
384 }
385 fn plot_area(&self) -> Rect;
386 fn total_area(&self) -> Rect;
387
388 /// Attach a tooltip to subsequently-drawn marks until cleared with `None`.
389 /// Backends that produce interactive output (SVG) emit it as a `<title>`
390 /// for a native hover tooltip; others ignore it. Default: no-op.
391 fn set_tooltip(&mut self, _tooltip: Option<String>) {}
392
393 /// Attach an axis (x) key to subsequently-drawn marks — SVG emits it as a
394 /// `data-x` attribute so a host can group marks by x (axis-pointer tooltip).
395 /// Default: no-op.
396 fn set_mark_axis(&mut self, _key: Option<String>) {}
397
398 /// Attach a series key (the colour / fill / group level) to subsequently
399 /// drawn marks — SVG emits it as `data-series`, so a host can highlight or
400 /// filter one series without parsing the tooltip. Default: no-op.
401 fn set_mark_series(&mut self, _series: Option<String>) {}
402
403 /// Attach the raw measured value (the unformatted y) to subsequently drawn
404 /// marks — SVG emits it as `data-value`. Default: no-op.
405 fn set_mark_value(&mut self, _value: Option<String>) {}
406
407 /// Tag subsequently drawn elements as data marks of the named geom (the
408 /// layer's `Geom::name`) until cleared with `None`. SVG emits
409 /// `class="gg-mark gg-<kind>"` (kind from the primitive: `point`, `line`,
410 /// `band`, `bar`, `text`) plus `data-geom`, so a host can select data
411 /// marks without matching axes, grid lines or legends. Default: no-op.
412 fn set_mark_geom(&mut self, _geom: Option<&str>) {}
413
414 /// Report a non-fatal problem found while drawing (e.g. labels a repel
415 /// layout had to drop). Backends that collect warnings (the native SVG
416 /// backend) append it to the render warnings. Default: ignored.
417 fn warn(&mut self, _message: String) {}
418
419 /// Mark subsequently-drawn marks as belonging to a sub-part of a larger
420 /// geom (e.g. `"arrow"` for an arrowhead drawn alongside a segment/curve/
421 /// path/line). While set, the SVG backend writes `data-part="<part>"
422 /// aria-hidden="true"` and omits the `<title>` tooltip (the part is
423 /// decorative; the parent mark already carries the semantic info).
424 /// `data-x`/`data-series`/`data-value` (and any user `data-*` attrs) are
425 /// still written. Default: no-op.
426 fn set_mark_part(&mut self, _part: Option<&str>) {}
427
428 /// Attach arbitrary `data-*` attributes to subsequently-drawn marks
429 /// (B1-7's `Aes::data_attr`). The SVG backend writes each, escaped; an
430 /// attr with the same name as a built-in (`data-x`, `data-series`,
431 /// `data-value`) replaces it rather than duplicating it. An empty
432 /// `Vec` clears any previously set attrs. Default: no-op.
433 fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
434
435 /// Whether the caller asked for per-mark formatted values
436 /// ([`crate::plot::GGPlot::svg_formatted_values`], B2-4) — a query a
437 /// geom checks before doing the (otherwise pointless) work of resolving
438 /// a scale's formatter for `set_mark_fvalue`/`set_mark_fx`. Default
439 /// false, so the feature is byte-identical when never turned on.
440 fn wants_formatted_values(&self) -> bool {
441 false
442 }
443
444 /// `data-fvalue` on subsequently-drawn marks: the "value" aesthetic's
445 /// raw data value (y for most geoms, fill for hex/tile), formatted
446 /// through that aesthetic's scale (`Scale::format_value`) — e.g. a
447 /// de-DE currency string. Only meaningful while
448 /// [`wants_formatted_values`](Self::wants_formatted_values) is true;
449 /// `None` clears any previous mark's value, per row. Default: no-op.
450 fn set_mark_fvalue(&mut self, _value: Option<String>) {}
451
452 /// `data-fx` on subsequently-drawn marks: the x value formatted through
453 /// the x scale (e.g. a datetime axis's ISO/locale date). The SVG
454 /// backend omits it when it matches the already-written `data-x`, so a
455 /// plain numeric x doesn't get a redundant duplicate attribute. Default:
456 /// no-op.
457 fn set_mark_fx(&mut self, _value: Option<String>) {}
458
459 /// Draw a point with a specific shape. Default delegates to draw_circle for Circle.
460 fn draw_shape(
461 &mut self,
462 center: (f64, f64),
463 radius: f64,
464 style: &PointStyle,
465 ) -> Result<(), RenderError> {
466 match style.shape {
467 PointShape::Circle => self.draw_circle(center, radius, style),
468 _ => {
469 // Default: fall back to circle for unsupported backends
470 self.draw_circle(center, radius, style)
471 }
472 }
473 }
474
475 /// Draw a quadratic Bézier curve from `p0` to `p1` with control point
476 /// `ctrl`, in pixel space. The default implementation flattens the curve
477 /// to 32 line segments and draws it with [`draw_line`](Self::draw_line);
478 /// backends that support real curves (SVG) should override this.
479 /// Non-finite input draws nothing.
480 fn draw_quadratic(
481 &mut self,
482 p0: (f64, f64),
483 ctrl: (f64, f64),
484 p1: (f64, f64),
485 style: &LineStyle,
486 ) -> Result<(), RenderError> {
487 if !(p0.0.is_finite()
488 && p0.1.is_finite()
489 && ctrl.0.is_finite()
490 && ctrl.1.is_finite()
491 && p1.0.is_finite()
492 && p1.1.is_finite())
493 {
494 return Ok(());
495 }
496 const STEPS: usize = 32;
497 let mut pts = Vec::with_capacity(STEPS + 1);
498 for i in 0..=STEPS {
499 let t = i as f64 / STEPS as f64;
500 let one_minus_t = 1.0 - t;
501 let x =
502 one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
503 let y =
504 one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
505 pts.push((x, y));
506 }
507 self.draw_line(&pts, style)
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::render::{Rect, RenderError};
515 use std::cell::RefCell;
516
517 /// A minimal backend that does not override `draw_quadratic`, to exercise
518 /// the trait's default flattening implementation.
519 /// One `draw_polygon` call's recorded points and fill colour.
520 type RecordedPolygon = (Vec<(f64, f64)>, Option<(u8, u8, u8)>);
521
522 #[derive(Default)]
523 struct RecordingBackend {
524 lines: RefCell<Vec<Vec<(f64, f64)>>>,
525 circles: RefCell<Vec<((f64, f64), f64)>>,
526 polygons: RefCell<Vec<RecordedPolygon>>,
527 }
528
529 impl DrawBackend for RecordingBackend {
530 fn draw_circle(
531 &mut self,
532 center: (f64, f64),
533 radius: f64,
534 _style: &PointStyle,
535 ) -> Result<(), RenderError> {
536 self.circles.borrow_mut().push((center, radius));
537 Ok(())
538 }
539 fn draw_line(
540 &mut self,
541 points: &[(f64, f64)],
542 _style: &LineStyle,
543 ) -> Result<(), RenderError> {
544 self.lines.borrow_mut().push(points.to_vec());
545 Ok(())
546 }
547 fn draw_rect(
548 &mut self,
549 _top_left: (f64, f64),
550 _bottom_right: (f64, f64),
551 _style: &RectStyle,
552 ) -> Result<(), RenderError> {
553 Ok(())
554 }
555 fn draw_text(
556 &mut self,
557 _text: &str,
558 _pos: (f64, f64),
559 _style: &TextStyle,
560 ) -> Result<(), RenderError> {
561 Ok(())
562 }
563 fn draw_polygon(
564 &mut self,
565 points: &[(f64, f64)],
566 style: &RectStyle,
567 ) -> Result<(), RenderError> {
568 self.polygons
569 .borrow_mut()
570 .push((points.to_vec(), style.fill));
571 Ok(())
572 }
573 fn plot_area(&self) -> Rect {
574 Rect {
575 x: 0.0,
576 y: 0.0,
577 width: 100.0,
578 height: 100.0,
579 }
580 }
581 fn total_area(&self) -> Rect {
582 self.plot_area()
583 }
584 }
585
586 #[test]
587 fn default_draw_quadratic_flattens_to_33_points() {
588 let mut b = RecordingBackend::default();
589 b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
590 .unwrap();
591 let lines = b.lines.borrow();
592 assert_eq!(lines.len(), 1);
593 assert_eq!(lines[0].len(), 33);
594 assert_eq!(lines[0][0], (0.0, 0.0));
595 assert_eq!(lines[0][32], (10.0, 0.0));
596 // Midpoint of a quadratic Bézier at t=0.5 is the average of the
597 // endpoints and the control point, each weighted 1/4, 1/2, 1/4.
598 let mid = lines[0][16];
599 assert!((mid.0 - 5.0).abs() < 1e-9);
600 assert!((mid.1 - 5.0).abs() < 1e-9);
601 }
602
603 #[test]
604 fn default_draw_quadratic_drops_non_finite() {
605 let mut b = RecordingBackend::default();
606 b.draw_quadratic(
607 (f64::NAN, 0.0),
608 (5.0, 10.0),
609 (10.0, 0.0),
610 &LineStyle::default(),
611 )
612 .unwrap();
613 assert!(b.lines.borrow().is_empty());
614 }
615
616 /// The default `draw_shape` falls back to `draw_circle` for every
617 /// `PointShape` (not just `Circle`) on a backend that doesn't override
618 /// it — e.g. a minimal backend that only implements the required
619 /// methods.
620 #[test]
621 fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
622 let mut b = RecordingBackend::default();
623 for shape in PointShape::ALL {
624 b.draw_shape(
625 (1.0, 2.0),
626 3.0,
627 &PointStyle {
628 shape: *shape,
629 ..Default::default()
630 },
631 )
632 .unwrap();
633 }
634 assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
635 for (center, radius) in b.circles.borrow().iter() {
636 assert_eq!(*center, (1.0, 2.0));
637 assert_eq!(*radius, 3.0);
638 }
639 }
640
641 /// The default `draw_polygon_rings` (for a backend pre-dating #178, or
642 /// any third-party `DrawBackend` that never overrides it) falls back to
643 /// one `draw_polygon` call per ring: the exterior filled, every other
644 /// ring (a hole) with its fill cleared — the old approximation, not a
645 /// real cut-out, but it draws *something* rather than nothing.
646 #[test]
647 fn default_draw_polygon_rings_fills_only_the_exterior() {
648 let mut b = RecordingBackend::default();
649 let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
650 let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
651 b.draw_polygon_rings(
652 &[exterior.clone(), hole.clone()],
653 &RectStyle {
654 fill: Some((200, 50, 50)),
655 stroke: Some((0, 0, 0)),
656 stroke_width: 1.0,
657 alpha: 1.0,
658 clip: true,
659 corner_radius: 0.0,
660 },
661 )
662 .unwrap();
663 let polys = b.polygons.borrow();
664 assert_eq!(polys.len(), 2, "one draw_polygon call per ring");
665 assert_eq!(polys[0], (exterior, Some((200, 50, 50))), "exterior filled");
666 assert_eq!(polys[1].0, hole);
667 assert_eq!(polys[1].1, None, "hole not filled (no real cut-out)");
668 }
669
670 #[test]
671 fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
672 let mut b = RecordingBackend::default();
673 let pa = b.plot_area();
674 let ta = b.total_area();
675 assert_eq!(
676 (pa.x, pa.y, pa.width, pa.height),
677 (ta.x, ta.y, ta.width, ta.height)
678 );
679 assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
680 b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
681 .unwrap();
682 b.draw_text("label", (0.0, 0.0), &TextStyle::default())
683 .unwrap();
684 b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
685 .unwrap();
686 }
687}