Skip to main content

ggplot_rs/theme/
elements.rs

1/// Text element styling.
2#[derive(Clone, Debug)]
3pub struct ElementText {
4    pub family: String,
5    /// Font face (R's `element_text(face = ...)`).
6    pub face: crate::render::backend::FontFace,
7    pub size: f64,
8    pub color: (u8, u8, u8),
9    pub angle: f64,
10    pub hjust: f64,
11    pub vjust: f64,
12    pub visible: bool,
13}
14
15impl ElementText {
16    /// Create an invisible (blank) text element.
17    pub fn blank() -> Self {
18        ElementText {
19            visible: false,
20            ..Default::default()
21        }
22    }
23}
24
25impl Default for ElementText {
26    fn default() -> Self {
27        ElementText {
28            family: "sans-serif".to_string(),
29            face: crate::render::backend::FontFace::Plain,
30            size: 12.0,
31            color: (0, 0, 0),
32            angle: 0.0,
33            hjust: 0.5,
34            vjust: 0.5,
35            visible: true,
36        }
37    }
38}
39
40/// Line element styling.
41#[derive(Clone, Debug)]
42pub struct ElementLine {
43    pub color: (u8, u8, u8),
44    pub width: f64,
45    pub visible: bool,
46    /// Line style (R's `element_line(linetype = ...)`).
47    pub linetype: crate::render::backend::Linetype,
48}
49
50impl ElementLine {
51    /// Create an invisible (blank) line element.
52    pub fn blank() -> Self {
53        ElementLine {
54            visible: false,
55            ..Default::default()
56        }
57    }
58}
59
60impl Default for ElementLine {
61    fn default() -> Self {
62        ElementLine {
63            color: (0, 0, 0),
64            width: 1.0,
65            visible: true,
66            linetype: crate::render::backend::Linetype::Solid,
67        }
68    }
69}
70
71/// Rectangle element styling.
72#[derive(Clone, Debug)]
73pub struct ElementRect {
74    pub fill: Option<(u8, u8, u8)>,
75    pub color: Option<(u8, u8, u8)>,
76    pub width: f64,
77    pub visible: bool,
78}
79
80impl ElementRect {
81    /// Create an invisible (blank) rect element.
82    pub fn blank() -> Self {
83        ElementRect {
84            visible: false,
85            ..Default::default()
86        }
87    }
88}
89
90impl Default for ElementRect {
91    fn default() -> Self {
92        ElementRect {
93            fill: Some((255, 255, 255)),
94            color: None,
95            width: 0.0,
96            visible: true,
97        }
98    }
99}
100
101/// Geom "ink"/"paper"/"accent" palette (ggplot2 4.0's `element_geom`,
102/// B2-7): recolours a geom's *built-in default* colour without touching
103/// every geom's own color/fill field or needing a mapped/explicit colour
104/// to be set first. `None` everywhere (the default) changes nothing —
105/// byte-identical output. Marked `#[non_exhaustive]` so a later field can
106/// be added without an `..Default::default()` break.
107///
108/// A default [`Geom::apply_element_geom`](crate::geom::Geom::apply_element_geom)
109/// is a no-op; a geom that honours this overrides it, replacing only
110/// fields still at their own built-in default — called once per layer,
111/// before the brand [`Theme::primary`](super::Theme::primary) colour is
112/// applied, and (like the brand colour) only for a layer that was *not*
113/// configured explicitly via `geom_*_with(...)`: that is the actual guard
114/// against touching an explicit colour (an `explicit_style` gate in the
115/// build pipeline), not a comparison of the field's *value* against the
116/// hard-coded default, which can't otherwise tell an explicitly-set black
117/// apart from a black nobody touched.
118///
119/// **Geoms covered:** `GeomPoint`, `GeomLine`, `GeomPath`, `GeomText`,
120/// `GeomLabel`, `GeomBoxplot`, `GeomSegment`, `GeomCurve`, the reference
121/// lines (`GeomHline`/`GeomVline`/`GeomAbline`), `GeomErrorbar`/
122/// `GeomErrorbarh`/`GeomLinerange`/`GeomPointrange`, `GeomStep`, the
123/// outline colour of `GeomRect`/`GeomTile`/`GeomBar`/`GeomCol`/
124/// `GeomPolygon`/`GeomViolin`/`GeomDensity`/`GeomArea`/`GeomCount`/
125/// `GeomHex`/`GeomContour`/`GeomHistogram`/`GeomDensity2d`/`GeomBin2d`/
126/// `GeomSf` (feature `sf`), `GeomCrossbar`, `GeomTextRepel`,
127/// `GeomLabelRepel`, `GeomSpoke`, `GeomRug`, `GeomBracket`, `GeomJitter`,
128/// `GeomQQ`, `GeomCensorMarks` and `GeomFreqpoly` (`ink` for a black/
129/// `(50, 50, 50)` line, text or outline; `paper` for a white fill, on
130/// `GeomBoxplot`/`GeomLabel`/`GeomCrossbar`/`GeomLabelRepel`) and
131/// `GeomSmooth` (`accent` for its default blue line and ribbon fill) —
132/// plus the legend key swatch colour, which reads `ink` directly rather
133/// than going through this trait method. A handful of geoms are left
134/// uncovered on purpose: `GeomDotplot`'s `color` field is dead config
135/// (never drawn); `GeomQQLine`/`GeomCandlestick`/`GeomOhlc`'s defaults are
136/// deliberate semantic colours, not neutral ones; `GeomQQBand`'s mid-grey
137/// fill doesn't match this type's black/white checks; and
138/// `GeomCalendarBorder` already reads its colour from the dedicated
139/// [`Theme::calendar_month_border`](super::Theme::calendar_month_border)
140/// element. See sipemu/ggplot-rs#142.
141#[non_exhaustive]
142#[derive(Clone, Debug, Default)]
143pub struct ElementGeom {
144    /// Replaces a geom's built-in black / `(50, 50, 50)` line, text or
145    /// outline colour (including the legend key colour).
146    pub ink: Option<(u8, u8, u8)>,
147    /// Replaces a geom's built-in white fill.
148    pub paper: Option<(u8, u8, u8)>,
149    /// Replaces the smooth line's built-in colour.
150    pub accent: Option<(u8, u8, u8)>,
151    /// Font family — the middle tier of B2-6's resolution order
152    /// (a geom's own `fontfamily`, then this, then `theme.text.family`).
153    pub family: Option<String>,
154}
155
156impl ElementGeom {
157    /// Builder for [`ink`](Self::ink) — `#[non_exhaustive]` blocks a
158    /// cross-crate struct literal even with `..Default::default()`, so
159    /// each field gets one of these.
160    pub fn with_ink(mut self, color: (u8, u8, u8)) -> Self {
161        self.ink = Some(color);
162        self
163    }
164
165    /// Builder for [`paper`](Self::paper).
166    pub fn with_paper(mut self, color: (u8, u8, u8)) -> Self {
167        self.paper = Some(color);
168        self
169    }
170
171    /// Builder for [`accent`](Self::accent).
172    pub fn with_accent(mut self, color: (u8, u8, u8)) -> Self {
173        self.accent = Some(color);
174        self
175    }
176
177    /// Builder for [`family`](Self::family).
178    pub fn with_family(mut self, family: &str) -> Self {
179        self.family = Some(family.to_string());
180        self
181    }
182}
183
184#[cfg(test)]
185mod element_geom_tests {
186    use super::ElementGeom;
187
188    #[test]
189    fn builders_set_every_field() {
190        let el = ElementGeom::default()
191            .with_ink((1, 2, 3))
192            .with_paper((4, 5, 6))
193            .with_accent((7, 8, 9))
194            .with_family("Georgia");
195        assert_eq!(el.ink, Some((1, 2, 3)));
196        assert_eq!(el.paper, Some((4, 5, 6)));
197        assert_eq!(el.accent, Some((7, 8, 9)));
198        assert_eq!(el.family, Some("Georgia".to_string()));
199    }
200}