Skip to main content

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
386    /// Draw one line as consecutive runs, each optionally an `http(s)`
387    /// hyperlink (ggspatial-style attribution: "© BKG" linked, a licence
388    /// code linked, plain connective text between them —
389    /// `annotation_attribution`'s `.link()`/`.runs()`, #196). `pos`/`style`
390    /// place and style the line as a whole, same as [`draw_text`](Self::draw_text)
391    /// — `style.anchor` aligns the *total* run sequence, not each run.
392    ///
393    /// Default: concatenates the runs' text and draws it plain with
394    /// [`draw_text`](Self::draw_text), dropping every link — the sensible
395    /// fallback for a backend with no link primitive (pixel/`canvas`,
396    /// `plotters`). The native SVG backend overrides this to wrap each
397    /// linked run in `<a href="…" target="_blank" rel="noopener">` with an
398    /// underline, inside one `<text>` element so the anchor still aligns
399    /// the whole line.
400    fn draw_text_runs(
401        &mut self,
402        runs: &[(&str, Option<&str>)],
403        pos: (f64, f64),
404        style: &TextStyle,
405    ) -> Result<(), RenderError> {
406        let joined: String = runs.iter().map(|(t, _)| *t).collect();
407        self.draw_text(&joined, pos, style)
408    }
409    fn plot_area(&self) -> Rect;
410    fn total_area(&self) -> Rect;
411
412    /// Attach a tooltip to subsequently-drawn marks until cleared with `None`.
413    /// Backends that produce interactive output (SVG) emit it as a `<title>`
414    /// for a native hover tooltip; others ignore it. Default: no-op.
415    fn set_tooltip(&mut self, _tooltip: Option<String>) {}
416
417    /// Attach an axis (x) key to subsequently-drawn marks — SVG emits it as a
418    /// `data-x` attribute so a host can group marks by x (axis-pointer tooltip).
419    /// Default: no-op.
420    fn set_mark_axis(&mut self, _key: Option<String>) {}
421
422    /// Attach a series key (the colour / fill / group level) to subsequently
423    /// drawn marks — SVG emits it as `data-series`, so a host can highlight or
424    /// filter one series without parsing the tooltip. Default: no-op.
425    fn set_mark_series(&mut self, _series: Option<String>) {}
426
427    /// Attach the raw measured value (the unformatted y) to subsequently drawn
428    /// marks — SVG emits it as `data-value`. Default: no-op.
429    fn set_mark_value(&mut self, _value: Option<String>) {}
430
431    /// Tag subsequently drawn elements as data marks of the named geom (the
432    /// layer's `Geom::name`) until cleared with `None`. SVG emits
433    /// `class="gg-mark gg-<kind>"` (kind from the primitive: `point`, `line`,
434    /// `band`, `bar`, `text`) plus `data-geom`, so a host can select data
435    /// marks without matching axes, grid lines or legends. Default: no-op.
436    fn set_mark_geom(&mut self, _geom: Option<&str>) {}
437
438    /// Report a non-fatal problem found while drawing (e.g. labels a repel
439    /// layout had to drop). Backends that collect warnings (the native SVG
440    /// backend) append it to the render warnings. Default: ignored.
441    fn warn(&mut self, _message: String) {}
442
443    /// Mark subsequently-drawn marks as belonging to a sub-part of a larger
444    /// geom (e.g. `"arrow"` for an arrowhead drawn alongside a segment/curve/
445    /// path/line). While set, the SVG backend writes `data-part="<part>"
446    /// aria-hidden="true"` and omits the `<title>` tooltip (the part is
447    /// decorative; the parent mark already carries the semantic info).
448    /// `data-x`/`data-series`/`data-value` (and any user `data-*` attrs) are
449    /// still written. Default: no-op.
450    fn set_mark_part(&mut self, _part: Option<&str>) {}
451
452    /// Attach arbitrary `data-*` attributes to subsequently-drawn marks
453    /// (B1-7's `Aes::data_attr`). The SVG backend writes each, escaped; an
454    /// attr with the same name as a built-in (`data-x`, `data-series`,
455    /// `data-value`) replaces it rather than duplicating it. An empty
456    /// `Vec` clears any previously set attrs. Default: no-op.
457    fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
458
459    /// Whether the caller asked for per-mark formatted values
460    /// ([`crate::plot::GGPlot::svg_formatted_values`], B2-4) — a query a
461    /// geom checks before doing the (otherwise pointless) work of resolving
462    /// a scale's formatter for `set_mark_fvalue`/`set_mark_fx`. Default
463    /// false, so the feature is byte-identical when never turned on.
464    fn wants_formatted_values(&self) -> bool {
465        false
466    }
467
468    /// `data-fvalue` on subsequently-drawn marks: the "value" aesthetic's
469    /// raw data value (y for most geoms, fill for hex/tile), formatted
470    /// through that aesthetic's scale (`Scale::format_value`) — e.g. a
471    /// de-DE currency string. Only meaningful while
472    /// [`wants_formatted_values`](Self::wants_formatted_values) is true;
473    /// `None` clears any previous mark's value, per row. Default: no-op.
474    fn set_mark_fvalue(&mut self, _value: Option<String>) {}
475
476    /// `data-fx` on subsequently-drawn marks: the x value formatted through
477    /// the x scale (e.g. a datetime axis's ISO/locale date). The SVG
478    /// backend omits it when it matches the already-written `data-x`, so a
479    /// plain numeric x doesn't get a redundant duplicate attribute. Default:
480    /// no-op.
481    fn set_mark_fx(&mut self, _value: Option<String>) {}
482
483    /// Draw a point with a specific shape. Default delegates to draw_circle for Circle.
484    fn draw_shape(
485        &mut self,
486        center: (f64, f64),
487        radius: f64,
488        style: &PointStyle,
489    ) -> Result<(), RenderError> {
490        match style.shape {
491            PointShape::Circle => self.draw_circle(center, radius, style),
492            _ => {
493                // Default: fall back to circle for unsupported backends
494                self.draw_circle(center, radius, style)
495            }
496        }
497    }
498
499    /// Draw a quadratic Bézier curve from `p0` to `p1` with control point
500    /// `ctrl`, in pixel space. The default implementation flattens the curve
501    /// to 32 line segments and draws it with [`draw_line`](Self::draw_line);
502    /// backends that support real curves (SVG) should override this.
503    /// Non-finite input draws nothing.
504    fn draw_quadratic(
505        &mut self,
506        p0: (f64, f64),
507        ctrl: (f64, f64),
508        p1: (f64, f64),
509        style: &LineStyle,
510    ) -> Result<(), RenderError> {
511        if !(p0.0.is_finite()
512            && p0.1.is_finite()
513            && ctrl.0.is_finite()
514            && ctrl.1.is_finite()
515            && p1.0.is_finite()
516            && p1.1.is_finite())
517        {
518            return Ok(());
519        }
520        const STEPS: usize = 32;
521        let mut pts = Vec::with_capacity(STEPS + 1);
522        for i in 0..=STEPS {
523            let t = i as f64 / STEPS as f64;
524            let one_minus_t = 1.0 - t;
525            let x =
526                one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
527            let y =
528                one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
529            pts.push((x, y));
530        }
531        self.draw_line(&pts, style)
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use crate::render::{Rect, RenderError};
539    use std::cell::RefCell;
540
541    /// A minimal backend that does not override `draw_quadratic`, to exercise
542    /// the trait's default flattening implementation.
543    /// One `draw_polygon` call's recorded points and fill colour.
544    type RecordedPolygon = (Vec<(f64, f64)>, Option<(u8, u8, u8)>);
545
546    #[derive(Default)]
547    struct RecordingBackend {
548        lines: RefCell<Vec<Vec<(f64, f64)>>>,
549        circles: RefCell<Vec<((f64, f64), f64)>>,
550        polygons: RefCell<Vec<RecordedPolygon>>,
551    }
552
553    impl DrawBackend for RecordingBackend {
554        fn draw_circle(
555            &mut self,
556            center: (f64, f64),
557            radius: f64,
558            _style: &PointStyle,
559        ) -> Result<(), RenderError> {
560            self.circles.borrow_mut().push((center, radius));
561            Ok(())
562        }
563        fn draw_line(
564            &mut self,
565            points: &[(f64, f64)],
566            _style: &LineStyle,
567        ) -> Result<(), RenderError> {
568            self.lines.borrow_mut().push(points.to_vec());
569            Ok(())
570        }
571        fn draw_rect(
572            &mut self,
573            _top_left: (f64, f64),
574            _bottom_right: (f64, f64),
575            _style: &RectStyle,
576        ) -> Result<(), RenderError> {
577            Ok(())
578        }
579        fn draw_text(
580            &mut self,
581            _text: &str,
582            _pos: (f64, f64),
583            _style: &TextStyle,
584        ) -> Result<(), RenderError> {
585            Ok(())
586        }
587        fn draw_polygon(
588            &mut self,
589            points: &[(f64, f64)],
590            style: &RectStyle,
591        ) -> Result<(), RenderError> {
592            self.polygons
593                .borrow_mut()
594                .push((points.to_vec(), style.fill));
595            Ok(())
596        }
597        fn plot_area(&self) -> Rect {
598            Rect {
599                x: 0.0,
600                y: 0.0,
601                width: 100.0,
602                height: 100.0,
603            }
604        }
605        fn total_area(&self) -> Rect {
606            self.plot_area()
607        }
608    }
609
610    #[test]
611    fn default_draw_quadratic_flattens_to_33_points() {
612        let mut b = RecordingBackend::default();
613        b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
614            .unwrap();
615        let lines = b.lines.borrow();
616        assert_eq!(lines.len(), 1);
617        assert_eq!(lines[0].len(), 33);
618        assert_eq!(lines[0][0], (0.0, 0.0));
619        assert_eq!(lines[0][32], (10.0, 0.0));
620        // Midpoint of a quadratic Bézier at t=0.5 is the average of the
621        // endpoints and the control point, each weighted 1/4, 1/2, 1/4.
622        let mid = lines[0][16];
623        assert!((mid.0 - 5.0).abs() < 1e-9);
624        assert!((mid.1 - 5.0).abs() < 1e-9);
625    }
626
627    #[test]
628    fn default_draw_quadratic_drops_non_finite() {
629        let mut b = RecordingBackend::default();
630        b.draw_quadratic(
631            (f64::NAN, 0.0),
632            (5.0, 10.0),
633            (10.0, 0.0),
634            &LineStyle::default(),
635        )
636        .unwrap();
637        assert!(b.lines.borrow().is_empty());
638    }
639
640    /// The default `draw_shape` falls back to `draw_circle` for every
641    /// `PointShape` (not just `Circle`) on a backend that doesn't override
642    /// it — e.g. a minimal backend that only implements the required
643    /// methods.
644    #[test]
645    fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
646        let mut b = RecordingBackend::default();
647        for shape in PointShape::ALL {
648            b.draw_shape(
649                (1.0, 2.0),
650                3.0,
651                &PointStyle {
652                    shape: *shape,
653                    ..Default::default()
654                },
655            )
656            .unwrap();
657        }
658        assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
659        for (center, radius) in b.circles.borrow().iter() {
660            assert_eq!(*center, (1.0, 2.0));
661            assert_eq!(*radius, 3.0);
662        }
663    }
664
665    /// The default `draw_polygon_rings` (for a backend pre-dating #178, or
666    /// any third-party `DrawBackend` that never overrides it) falls back to
667    /// one `draw_polygon` call per ring: the exterior filled, every other
668    /// ring (a hole) with its fill cleared — the old approximation, not a
669    /// real cut-out, but it draws *something* rather than nothing.
670    #[test]
671    fn default_draw_polygon_rings_fills_only_the_exterior() {
672        let mut b = RecordingBackend::default();
673        let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
674        let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
675        b.draw_polygon_rings(
676            &[exterior.clone(), hole.clone()],
677            &RectStyle {
678                fill: Some((200, 50, 50)),
679                stroke: Some((0, 0, 0)),
680                stroke_width: 1.0,
681                alpha: 1.0,
682                clip: true,
683                corner_radius: 0.0,
684            },
685        )
686        .unwrap();
687        let polys = b.polygons.borrow();
688        assert_eq!(polys.len(), 2, "one draw_polygon call per ring");
689        assert_eq!(polys[0], (exterior, Some((200, 50, 50))), "exterior filled");
690        assert_eq!(polys[1].0, hole);
691        assert_eq!(polys[1].1, None, "hole not filled (no real cut-out)");
692    }
693
694    #[test]
695    fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
696        let mut b = RecordingBackend::default();
697        let pa = b.plot_area();
698        let ta = b.total_area();
699        assert_eq!(
700            (pa.x, pa.y, pa.width, pa.height),
701            (ta.x, ta.y, ta.width, ta.height)
702        );
703        assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
704        b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
705            .unwrap();
706        b.draw_text("label", (0.0, 0.0), &TextStyle::default())
707            .unwrap();
708        b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
709            .unwrap();
710    }
711}