1use 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
56pub struct BorderSide {
57 #[serde(default)]
59 pub line_type: BorderLineType,
60
61 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
101pub struct Border {
102 #[serde(default)]
104 pub top: BorderSide,
105 #[serde(default)]
107 pub left: BorderSide,
108 #[serde(default)]
110 pub right: BorderSide,
111 #[serde(default)]
113 pub bottom: BorderSide,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
135pub struct Fill {
136 #[serde(default)]
138 pub brush_type: FillBrushType,
139
140 #[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 #[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#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
184pub struct PartialBorderFill {
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub border: Option<Border>,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub fill: Option<Fill>,
192}
193
194impl PartialBorderFill {
195 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 pub fn resolve(&self) -> BorderFill {
236 BorderFill { border: self.border.unwrap_or_default(), fill: self.fill.unwrap_or_default() }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
259pub struct BorderFill {
260 pub border: Border,
262 pub fill: Fill,
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use pretty_assertions::assert_eq;
270
271 #[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 #[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 #[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 #[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()); assert!(base.fill.is_some()); }
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()), fill: None,
444 };
445 base.merge(&child);
446 assert_eq!(base.border.unwrap().top.line_type, BorderLineType::None); }
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 #[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 #[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}