Skip to main content

r/interpreter/grid/
gpar.rs

1//! Grid graphical parameters (`gpar`) — per-grob styling with inheritance.
2//!
3//! Unlike base R's `par()` which is a global state, grid's `gpar()` creates
4//! immutable parameter sets that are attached to individual grobs and viewports.
5//! When a parameter is `None`, it inherits from the parent viewport's gpar.
6//!
7//! This module reuses `LineType` and `FontFace` from `graphics::par` to avoid
8//! duplication.
9
10use crate::interpreter::graphics::par::{FontFace, LineType};
11
12// region: Gpar
13
14/// Grid graphical parameter set.
15///
16/// Each field is `Option` — `None` means "inherit from parent viewport".
17/// Use `inherit_from()` to fill in missing values from a parent `Gpar`,
18/// or use the `effective_*()` methods to get resolved values with defaults.
19#[derive(Clone, Debug, Default)]
20pub struct Gpar {
21    /// Stroke color as RGBA.
22    pub col: Option<[u8; 4]>,
23    /// Fill color as RGBA.
24    pub fill: Option<[u8; 4]>,
25    /// Line width (default 1.0).
26    pub lwd: Option<f64>,
27    /// Line type (solid, dashed, etc.).
28    pub lty: Option<LineType>,
29    /// Font size in points (default 12.0).
30    pub fontsize: Option<f64>,
31    /// Line height multiplier (default 1.2).
32    pub lineheight: Option<f64>,
33    /// Font face (plain, bold, italic, etc.).
34    pub font: Option<FontFace>,
35    /// Font family name.
36    pub fontfamily: Option<String>,
37    /// Character expansion factor (multiplier on fontsize).
38    pub cex: Option<f64>,
39    /// Alpha transparency (0.0 = fully transparent, 1.0 = fully opaque).
40    pub alpha: Option<f64>,
41    /// Line end style.
42    pub lineend: Option<LineEnd>,
43    /// Line join style.
44    pub linejoin: Option<LineJoin>,
45    /// Mitre limit for line joins.
46    pub linemitre: Option<f64>,
47    /// Horizontal justification (0 = left, 0.5 = center, 1 = right).
48    pub just_x: Option<f64>,
49    /// Vertical justification (0 = bottom, 0.5 = center, 1 = top).
50    pub just_y: Option<f64>,
51}
52
53/// Line end cap styles, matching R's `lineend` parameter.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum LineEnd {
56    Round,
57    Butt,
58    Square,
59}
60
61/// Line join styles, matching R's `linejoin` parameter.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum LineJoin {
64    Round,
65    Mitre,
66    Bevel,
67}
68
69// Default RGBA constants
70const BLACK_RGBA: [u8; 4] = [0, 0, 0, 255];
71const TRANSPARENT_RGBA: [u8; 4] = [255, 255, 255, 0];
72
73impl Gpar {
74    /// Create a new empty gpar (all fields `None`).
75    pub fn new() -> Self {
76        Gpar::default()
77    }
78
79    /// Fill in any `None` fields from the parent gpar.
80    ///
81    /// This implements grid's inheritance: child viewports/grobs inherit
82    /// graphical parameters from their parent when not explicitly set.
83    pub fn inherit_from(&mut self, parent: &Gpar) {
84        if self.col.is_none() {
85            self.col = parent.col;
86        }
87        if self.fill.is_none() {
88            self.fill = parent.fill;
89        }
90        if self.lwd.is_none() {
91            self.lwd = parent.lwd;
92        }
93        if self.lty.is_none() {
94            self.lty = parent.lty;
95        }
96        if self.fontsize.is_none() {
97            self.fontsize = parent.fontsize;
98        }
99        if self.lineheight.is_none() {
100            self.lineheight = parent.lineheight;
101        }
102        if self.font.is_none() {
103            self.font = parent.font;
104        }
105        if self.fontfamily.is_none() {
106            self.fontfamily.clone_from(&parent.fontfamily);
107        }
108        if self.cex.is_none() {
109            self.cex = parent.cex;
110        }
111        if self.alpha.is_none() {
112            self.alpha = parent.alpha;
113        }
114        if self.lineend.is_none() {
115            self.lineend = parent.lineend;
116        }
117        if self.linejoin.is_none() {
118            self.linejoin = parent.linejoin;
119        }
120        if self.linemitre.is_none() {
121            self.linemitre = parent.linemitre;
122        }
123        if self.just_x.is_none() {
124            self.just_x = parent.just_x;
125        }
126        if self.just_y.is_none() {
127            self.just_y = parent.just_y;
128        }
129    }
130
131    /// Create a new gpar that is this gpar with parent values filled in.
132    pub fn with_parent(&self, parent: &Gpar) -> Gpar {
133        let mut result = self.clone();
134        result.inherit_from(parent);
135        result
136    }
137
138    /// Effective stroke color: this gpar's col, or black.
139    pub fn effective_col(&self) -> [u8; 4] {
140        self.col.unwrap_or(BLACK_RGBA)
141    }
142
143    /// Effective fill color: this gpar's fill, or transparent.
144    pub fn effective_fill(&self) -> [u8; 4] {
145        self.fill.unwrap_or(TRANSPARENT_RGBA)
146    }
147
148    /// Effective line width: this gpar's lwd, or 1.0.
149    pub fn effective_lwd(&self) -> f64 {
150        self.lwd.unwrap_or(1.0)
151    }
152
153    /// Effective line type: this gpar's lty, or Solid.
154    pub fn effective_lty(&self) -> LineType {
155        self.lty.unwrap_or(LineType::Solid)
156    }
157
158    /// Effective font size in points: this gpar's fontsize, or 12.0.
159    pub fn effective_fontsize(&self) -> f64 {
160        self.fontsize.unwrap_or(12.0)
161    }
162
163    /// Effective line height multiplier: this gpar's lineheight, or 1.2.
164    pub fn effective_lineheight(&self) -> f64 {
165        self.lineheight.unwrap_or(1.2)
166    }
167
168    /// Effective font face: this gpar's font, or Plain.
169    pub fn effective_font(&self) -> FontFace {
170        self.font.unwrap_or(FontFace::Plain)
171    }
172
173    /// Effective font family: this gpar's fontfamily, or "sans".
174    pub fn effective_fontfamily(&self) -> &str {
175        self.fontfamily.as_deref().unwrap_or("sans")
176    }
177
178    /// Effective character expansion factor: this gpar's cex, or 1.0.
179    pub fn effective_cex(&self) -> f64 {
180        self.cex.unwrap_or(1.0)
181    }
182
183    /// Effective alpha: this gpar's alpha, or 1.0 (fully opaque).
184    pub fn effective_alpha(&self) -> f64 {
185        self.alpha.unwrap_or(1.0)
186    }
187
188    /// Effective line end style: this gpar's lineend, or Round.
189    pub fn effective_lineend(&self) -> LineEnd {
190        self.lineend.unwrap_or(LineEnd::Round)
191    }
192
193    /// Effective line join style: this gpar's linejoin, or Round.
194    pub fn effective_linejoin(&self) -> LineJoin {
195        self.linejoin.unwrap_or(LineJoin::Round)
196    }
197
198    /// Effective mitre limit: this gpar's linemitre, or 10.0 (R default).
199    pub fn effective_linemitre(&self) -> f64 {
200        self.linemitre.unwrap_or(10.0)
201    }
202}
203
204// endregion
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn gpar_defaults() {
212        let g = Gpar::new();
213        assert_eq!(g.effective_col(), BLACK_RGBA);
214        assert_eq!(g.effective_fill(), TRANSPARENT_RGBA);
215        assert!((g.effective_lwd() - 1.0).abs() < f64::EPSILON);
216        assert!((g.effective_fontsize() - 12.0).abs() < f64::EPSILON);
217        assert!((g.effective_lineheight() - 1.2).abs() < f64::EPSILON);
218        assert_eq!(g.effective_font(), FontFace::Plain);
219        assert_eq!(g.effective_fontfamily(), "sans");
220        assert!((g.effective_cex() - 1.0).abs() < f64::EPSILON);
221        assert!((g.effective_alpha() - 1.0).abs() < f64::EPSILON);
222        assert_eq!(g.effective_lty(), LineType::Solid);
223        assert_eq!(g.effective_lineend(), LineEnd::Round);
224        assert_eq!(g.effective_linejoin(), LineJoin::Round);
225        assert!((g.effective_linemitre() - 10.0).abs() < f64::EPSILON);
226    }
227
228    #[test]
229    fn gpar_inherit_fills_none_fields() {
230        let parent = Gpar {
231            col: Some([255, 0, 0, 255]),
232            fontsize: Some(16.0),
233            lwd: Some(2.5),
234            fontfamily: Some("serif".to_string()),
235            ..Default::default()
236        };
237
238        let mut child = Gpar {
239            fontsize: Some(10.0), // child overrides fontsize
240            ..Default::default()
241        };
242
243        child.inherit_from(&parent);
244
245        // Inherited from parent
246        assert_eq!(child.col, Some([255, 0, 0, 255]));
247        assert_eq!(child.lwd, Some(2.5));
248        assert_eq!(child.fontfamily, Some("serif".to_string()));
249
250        // Child's own value preserved
251        assert_eq!(child.fontsize, Some(10.0));
252    }
253
254    #[test]
255    fn gpar_with_parent_does_not_mutate_original() {
256        let parent = Gpar {
257            col: Some([0, 255, 0, 255]),
258            ..Default::default()
259        };
260        let child = Gpar {
261            lwd: Some(3.0),
262            ..Default::default()
263        };
264
265        let resolved = child.with_parent(&parent);
266
267        // resolved has both parent and child values
268        assert_eq!(resolved.col, Some([0, 255, 0, 255]));
269        assert_eq!(resolved.lwd, Some(3.0));
270
271        // original child is unchanged
272        assert!(child.col.is_none());
273    }
274
275    #[test]
276    fn gpar_child_overrides_parent() {
277        let parent = Gpar {
278            col: Some([255, 0, 0, 255]),
279            fontsize: Some(16.0),
280            ..Default::default()
281        };
282
283        let child = Gpar {
284            col: Some([0, 0, 255, 255]),
285            ..Default::default()
286        };
287
288        let resolved = child.with_parent(&parent);
289
290        // Child's col wins
291        assert_eq!(resolved.col, Some([0, 0, 255, 255]));
292        // Parent's fontsize inherited
293        assert_eq!(resolved.fontsize, Some(16.0));
294    }
295}