hwpforge_blueprint/
border_fill.rs

1//! Border and fill types for paragraph and table styling.
2//!
3//! This module defines border (line style around elements) and fill (background)
4//! configurations following the **two-type pattern**:
5//!
6//! - [`PartialBorderFill`] — all fields `Option`, for YAML and inheritance
7//! - [`BorderFill`] — all fields required, after resolution
8//!
9//! # Examples
10//!
11//! ```
12//! use hwpforge_blueprint::border_fill::{PartialBorderFill, BorderSide, Fill};
13//! use hwpforge_foundation::{BorderLineType, FillBrushType, Color, HwpUnit};
14//!
15//! let mut partial = PartialBorderFill::default();
16//! partial.border = Some(hwpforge_blueprint::border_fill::Border {
17//!     top: BorderSide {
18//!         line_type: BorderLineType::Solid,
19//!         width: Some(HwpUnit::from_pt(0.5).unwrap()),
20//!         color: Some(Color::BLACK),
21//!     },
22//!     left: BorderSide::default(),
23//!     right: BorderSide::default(),
24//!     bottom: BorderSide::default(),
25//! });
26//!
27//! let resolved = partial.resolve();
28//! assert_eq!(resolved.border.top.line_type, BorderLineType::Solid);
29//! ```
30
31use hwpforge_foundation::{BorderLineType, Color, FillBrushType, HwpUnit};
32use schemars::JsonSchema;
33use serde::{Deserialize, Serialize};
34
35use crate::serde_helpers::{de_color_opt, de_dim_opt, ser_color_opt, ser_dim_opt};
36
37// ---------------------------------------------------------------------------
38// BorderSide
39// ---------------------------------------------------------------------------
40
41/// Border configuration for one side (top/left/right/bottom).
42///
43/// # Examples
44///
45/// ```
46/// use hwpforge_blueprint::border_fill::BorderSide;
47/// use hwpforge_foundation::{BorderLineType, Color, HwpUnit};
48///
49/// let side = BorderSide {
50///     line_type: BorderLineType::Solid,
51///     width: Some(HwpUnit::from_pt(1.0).unwrap()),
52///     color: Some(Color::BLACK),
53/// };
54/// ```
55#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
56pub struct BorderSide {
57    /// Border line type.
58    #[serde(default)]
59    pub line_type: BorderLineType,
60
61    /// Border line width.
62    #[serde(
63        default,
64        serialize_with = "ser_dim_opt",
65        deserialize_with = "de_dim_opt",
66        skip_serializing_if = "Option::is_none"
67    )]
68    pub width: Option<HwpUnit>,
69
70    /// Border color.
71    #[serde(
72        default,
73        serialize_with = "ser_color_opt",
74        deserialize_with = "de_color_opt",
75        skip_serializing_if = "Option::is_none"
76    )]
77    pub color: Option<Color>,
78}
79
80impl Default for BorderSide {
81    fn default() -> Self {
82        Self { line_type: BorderLineType::None, width: None, color: None }
83    }
84}
85
86// ---------------------------------------------------------------------------
87// Border
88// ---------------------------------------------------------------------------
89
90/// Four-sided border configuration.
91///
92/// # Examples
93///
94/// ```
95/// use hwpforge_blueprint::border_fill::Border;
96///
97/// let border = Border::default();
98/// assert_eq!(border.top.line_type, hwpforge_foundation::BorderLineType::None);
99/// ```
100#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
101pub struct Border {
102    /// Top border.
103    #[serde(default)]
104    pub top: BorderSide,
105    /// Left border.
106    #[serde(default)]
107    pub left: BorderSide,
108    /// Right border.
109    #[serde(default)]
110    pub right: BorderSide,
111    /// Bottom border.
112    #[serde(default)]
113    pub bottom: BorderSide,
114}
115
116// ---------------------------------------------------------------------------
117// Fill
118// ---------------------------------------------------------------------------
119
120/// Background fill configuration.
121///
122/// # Examples
123///
124/// ```
125/// use hwpforge_blueprint::border_fill::Fill;
126/// use hwpforge_foundation::{FillBrushType, Color};
127///
128/// let fill = Fill {
129///     brush_type: FillBrushType::Solid,
130///     color: Some(Color::from_rgb(0xF0, 0xF0, 0xF0)),
131///     color2: None,
132/// };
133/// ```
134#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
135pub struct Fill {
136    /// Fill type.
137    #[serde(default)]
138    pub brush_type: FillBrushType,
139
140    /// Primary fill color.
141    #[serde(
142        default,
143        serialize_with = "ser_color_opt",
144        deserialize_with = "de_color_opt",
145        skip_serializing_if = "Option::is_none"
146    )]
147    pub color: Option<Color>,
148
149    /// Secondary color for gradients/patterns.
150    #[serde(
151        default,
152        serialize_with = "ser_color_opt",
153        deserialize_with = "de_color_opt",
154        skip_serializing_if = "Option::is_none"
155    )]
156    pub color2: Option<Color>,
157}
158
159impl Default for Fill {
160    fn default() -> Self {
161        Self { brush_type: FillBrushType::None, color: None, color2: None }
162    }
163}
164
165// ---------------------------------------------------------------------------
166// PartialBorderFill (for YAML and inheritance)
167// ---------------------------------------------------------------------------
168
169/// Combined border and fill configuration with optional fields for YAML.
170///
171/// After inheritance resolution, this is converted to [`BorderFill`] where
172/// all fields are guaranteed to be present.
173///
174/// # Examples
175///
176/// ```
177/// use hwpforge_blueprint::border_fill::PartialBorderFill;
178///
179/// let partial = PartialBorderFill::default();
180/// let resolved = partial.resolve();
181/// assert_eq!(resolved.border.top.line_type, hwpforge_foundation::BorderLineType::None);
182/// ```
183#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
184pub struct PartialBorderFill {
185    /// Border configuration.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub border: Option<Border>,
188
189    /// Fill configuration.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub fill: Option<Fill>,
192}
193
194impl PartialBorderFill {
195    /// Merges `other` into `self` (child overrides parent).
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use hwpforge_blueprint::border_fill::{PartialBorderFill, Fill};
201    /// use hwpforge_foundation::{FillBrushType, Color};
202    ///
203    /// let mut base = PartialBorderFill::default();
204    /// let child = PartialBorderFill {
205    ///     fill: Some(Fill {
206    ///         brush_type: FillBrushType::Solid,
207    ///         color: Some(Color::WHITE),
208    ///         color2: None,
209    ///     }),
210    ///     ..Default::default()
211    /// };
212    /// base.merge(&child);
213    /// assert!(base.fill.is_some());
214    /// ```
215    pub fn merge(&mut self, other: &PartialBorderFill) {
216        if other.border.is_some() {
217            self.border = other.border;
218        }
219        if other.fill.is_some() {
220            self.fill = other.fill;
221        }
222    }
223
224    /// Resolves into a fully-specified [`BorderFill`] with defaults.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use hwpforge_blueprint::border_fill::PartialBorderFill;
230    ///
231    /// let partial = PartialBorderFill::default();
232    /// let resolved = partial.resolve();
233    /// assert_eq!(resolved.fill.brush_type, hwpforge_foundation::FillBrushType::None);
234    /// ```
235    pub fn resolve(&self) -> BorderFill {
236        BorderFill { border: self.border.unwrap_or_default(), fill: self.fill.unwrap_or_default() }
237    }
238}
239
240// ---------------------------------------------------------------------------
241// BorderFill (resolved, fully-specified)
242// ---------------------------------------------------------------------------
243
244/// Fully-resolved border and fill configuration (all fields present).
245///
246/// Created from [`PartialBorderFill`] after inheritance resolution.
247///
248/// # Examples
249///
250/// ```
251/// use hwpforge_blueprint::border_fill::{BorderFill, Border, Fill};
252///
253/// let bf = BorderFill {
254///     border: Border::default(),
255///     fill: Fill::default(),
256/// };
257/// ```
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
259pub struct BorderFill {
260    /// Border configuration.
261    pub border: Border,
262    /// Fill configuration.
263    pub fill: Fill,
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use pretty_assertions::assert_eq;
270
271    // ===================================================================
272    // BorderSide
273    // ===================================================================
274
275    #[test]
276    fn border_side_default_is_none() {
277        let side = BorderSide::default();
278        assert_eq!(side.line_type, BorderLineType::None);
279        assert!(side.width.is_none());
280        assert!(side.color.is_none());
281    }
282
283    #[test]
284    fn border_side_with_values() {
285        let side = BorderSide {
286            line_type: BorderLineType::Solid,
287            width: Some(HwpUnit::from_pt(1.0).unwrap()),
288            color: Some(Color::BLACK),
289        };
290        assert_eq!(side.line_type, BorderLineType::Solid);
291        assert_eq!(side.width, Some(HwpUnit::from_pt(1.0).unwrap()));
292        assert_eq!(side.color, Some(Color::BLACK));
293    }
294
295    #[test]
296    fn border_side_serde_roundtrip() {
297        let side = BorderSide {
298            line_type: BorderLineType::Dash,
299            width: Some(HwpUnit::from_pt(0.5).unwrap()),
300            color: Some(Color::from_rgb(0x33, 0x66, 0x99)),
301        };
302        let yaml = serde_yaml::to_string(&side).unwrap();
303        let back: BorderSide = serde_yaml::from_str(&yaml).unwrap();
304        assert_eq!(side, back);
305    }
306
307    // ===================================================================
308    // Border
309    // ===================================================================
310
311    #[test]
312    fn border_default_all_sides_none() {
313        let border = Border::default();
314        assert_eq!(border.top.line_type, BorderLineType::None);
315        assert_eq!(border.left.line_type, BorderLineType::None);
316        assert_eq!(border.right.line_type, BorderLineType::None);
317        assert_eq!(border.bottom.line_type, BorderLineType::None);
318    }
319
320    #[test]
321    fn border_with_top_only() {
322        let border = Border {
323            top: BorderSide {
324                line_type: BorderLineType::Solid,
325                width: Some(HwpUnit::from_pt(1.0).unwrap()),
326                color: Some(Color::BLACK),
327            },
328            ..Default::default()
329        };
330        assert_eq!(border.top.line_type, BorderLineType::Solid);
331        assert_eq!(border.left.line_type, BorderLineType::None);
332    }
333
334    #[test]
335    fn border_serde_roundtrip() {
336        let border = Border {
337            top: BorderSide {
338                line_type: BorderLineType::Solid,
339                width: Some(HwpUnit::from_pt(1.0).unwrap()),
340                color: Some(Color::BLACK),
341            },
342            bottom: BorderSide {
343                line_type: BorderLineType::Dash,
344                width: Some(HwpUnit::from_pt(0.5).unwrap()),
345                color: Some(Color::from_rgb(0xFF, 0x00, 0x00)),
346            },
347            ..Default::default()
348        };
349        let yaml = serde_yaml::to_string(&border).unwrap();
350        let back: Border = serde_yaml::from_str(&yaml).unwrap();
351        assert_eq!(border, back);
352    }
353
354    // ===================================================================
355    // Fill
356    // ===================================================================
357
358    #[test]
359    fn fill_default_is_none() {
360        let fill = Fill::default();
361        assert_eq!(fill.brush_type, FillBrushType::None);
362        assert!(fill.color.is_none());
363        assert!(fill.color2.is_none());
364    }
365
366    #[test]
367    fn fill_solid_color() {
368        let fill = Fill {
369            brush_type: FillBrushType::Solid,
370            color: Some(Color::from_rgb(0xF0, 0xF0, 0xF0)),
371            color2: None,
372        };
373        assert_eq!(fill.brush_type, FillBrushType::Solid);
374        assert_eq!(fill.color, Some(Color::from_rgb(0xF0, 0xF0, 0xF0)));
375    }
376
377    #[test]
378    fn fill_gradient_two_colors() {
379        let fill = Fill {
380            brush_type: FillBrushType::Gradient,
381            color: Some(Color::WHITE),
382            color2: Some(Color::BLACK),
383        };
384        assert_eq!(fill.brush_type, FillBrushType::Gradient);
385        assert_eq!(fill.color, Some(Color::WHITE));
386        assert_eq!(fill.color2, Some(Color::BLACK));
387    }
388
389    #[test]
390    fn fill_serde_roundtrip() {
391        let fill = Fill {
392            brush_type: FillBrushType::Gradient,
393            color: Some(Color::from_rgb(0xFF, 0xFF, 0x00)),
394            color2: Some(Color::from_rgb(0x00, 0xFF, 0xFF)),
395        };
396        let yaml = serde_yaml::to_string(&fill).unwrap();
397        let back: Fill = serde_yaml::from_str(&yaml).unwrap();
398        assert_eq!(fill, back);
399    }
400
401    // ===================================================================
402    // PartialBorderFill
403    // ===================================================================
404
405    #[test]
406    fn partial_border_fill_default_is_all_none() {
407        let partial = PartialBorderFill::default();
408        assert!(partial.border.is_none());
409        assert!(partial.fill.is_none());
410    }
411
412    #[test]
413    fn partial_border_fill_merge_overrides() {
414        let mut base = PartialBorderFill { border: Some(Border::default()), fill: None };
415        let child = PartialBorderFill {
416            border: None,
417            fill: Some(Fill {
418                brush_type: FillBrushType::Solid,
419                color: Some(Color::WHITE),
420                color2: None,
421            }),
422        };
423        base.merge(&child);
424        assert!(base.border.is_some()); // Preserved from base
425        assert!(base.fill.is_some()); // Added from child
426    }
427
428    #[test]
429    fn partial_border_fill_merge_child_replaces() {
430        let mut base = PartialBorderFill {
431            border: Some(Border {
432                top: BorderSide {
433                    line_type: BorderLineType::Solid,
434                    width: Some(HwpUnit::from_pt(1.0).unwrap()),
435                    color: Some(Color::BLACK),
436                },
437                ..Default::default()
438            }),
439            fill: None,
440        };
441        let child = PartialBorderFill {
442            border: Some(Border::default()), // Replace with default
443            fill: None,
444        };
445        base.merge(&child);
446        assert_eq!(base.border.unwrap().top.line_type, BorderLineType::None); // Replaced
447    }
448
449    #[test]
450    fn partial_border_fill_resolve_defaults() {
451        let partial = PartialBorderFill::default();
452        let resolved = partial.resolve();
453        assert_eq!(resolved.border.top.line_type, BorderLineType::None);
454        assert_eq!(resolved.fill.brush_type, FillBrushType::None);
455    }
456
457    #[test]
458    fn partial_border_fill_resolve_with_values() {
459        let partial = PartialBorderFill {
460            border: Some(Border {
461                top: BorderSide {
462                    line_type: BorderLineType::Solid,
463                    width: Some(HwpUnit::from_pt(1.0).unwrap()),
464                    color: Some(Color::BLACK),
465                },
466                ..Default::default()
467            }),
468            fill: Some(Fill {
469                brush_type: FillBrushType::Solid,
470                color: Some(Color::from_rgb(0xF0, 0xF0, 0xF0)),
471                color2: None,
472            }),
473        };
474        let resolved = partial.resolve();
475        assert_eq!(resolved.border.top.line_type, BorderLineType::Solid);
476        assert_eq!(resolved.fill.brush_type, FillBrushType::Solid);
477        assert_eq!(resolved.fill.color, Some(Color::from_rgb(0xF0, 0xF0, 0xF0)));
478    }
479
480    #[test]
481    fn partial_border_fill_serde_roundtrip() {
482        let partial = PartialBorderFill {
483            border: Some(Border {
484                top: BorderSide {
485                    line_type: BorderLineType::Solid,
486                    width: Some(HwpUnit::from_pt(1.0).unwrap()),
487                    color: Some(Color::BLACK),
488                },
489                ..Default::default()
490            }),
491            fill: Some(Fill {
492                brush_type: FillBrushType::Solid,
493                color: Some(Color::WHITE),
494                color2: None,
495            }),
496        };
497        let yaml = serde_yaml::to_string(&partial).unwrap();
498        let back: PartialBorderFill = serde_yaml::from_str(&yaml).unwrap();
499        assert_eq!(partial, back);
500    }
501
502    // ===================================================================
503    // BorderFill
504    // ===================================================================
505
506    #[test]
507    fn border_fill_construction() {
508        let bf = BorderFill { border: Border::default(), fill: Fill::default() };
509        assert_eq!(bf.border.top.line_type, BorderLineType::None);
510        assert_eq!(bf.fill.brush_type, FillBrushType::None);
511    }
512
513    #[test]
514    fn border_fill_serde_roundtrip() {
515        let bf = BorderFill {
516            border: Border {
517                top: BorderSide {
518                    line_type: BorderLineType::Solid,
519                    width: Some(HwpUnit::from_pt(1.0).unwrap()),
520                    color: Some(Color::BLACK),
521                },
522                ..Default::default()
523            },
524            fill: Fill {
525                brush_type: FillBrushType::Gradient,
526                color: Some(Color::WHITE),
527                color2: Some(Color::BLACK),
528            },
529        };
530        let yaml = serde_yaml::to_string(&bf).unwrap();
531        let back: BorderFill = serde_yaml::from_str(&yaml).unwrap();
532        assert_eq!(bf, back);
533    }
534
535    // ===================================================================
536    // YAML examples
537    // ===================================================================
538
539    #[test]
540    fn partial_border_fill_from_yaml() {
541        let yaml = r#"
542border:
543  top:
544    line_type: Solid
545    width: 1pt
546    color: '#000000'
547  bottom:
548    line_type: Dash
549    width: 0.5pt
550    color: '#FF0000'
551fill:
552  brush_type: Solid
553  color: '#F0F0F0'
554"#;
555        let partial: PartialBorderFill = serde_yaml::from_str(yaml).unwrap();
556        assert!(partial.border.is_some());
557        let border = partial.border.unwrap();
558        assert_eq!(border.top.line_type, BorderLineType::Solid);
559        assert_eq!(border.bottom.line_type, BorderLineType::Dash);
560        assert!(partial.fill.is_some());
561        assert_eq!(partial.fill.unwrap().brush_type, FillBrushType::Solid);
562    }
563}