use hwpforge_foundation::{BorderLineType, Color, HwpUnit};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum ColumnType {
#[default]
Newspaper,
Parallel,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum ColumnLayoutMode {
#[default]
Left,
Right,
Mirror,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ColumnDef {
pub width: HwpUnit,
pub gap: HwpUnit,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ColumnLine {
pub line_type: BorderLineType,
pub width: HwpUnit,
pub color: Color,
}
impl Default for ColumnLine {
fn default() -> Self {
Self {
line_type: BorderLineType::Solid,
width: HwpUnit::from_mm(0.12).unwrap_or(HwpUnit::ZERO),
color: Color::from_rgb(0, 0, 0),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ColumnSettings {
pub column_type: ColumnType,
pub layout_mode: ColumnLayoutMode,
pub columns: Vec<ColumnDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub col_line: Option<ColumnLine>,
}
impl ColumnSettings {
pub fn equal_columns(count: u32, gap: HwpUnit) -> Result<Self, &'static str> {
if count < 2 {
return Err("column count must be >= 2 (use None for single column)");
}
let columns: Vec<ColumnDef> = (0..count)
.map(|i| ColumnDef {
width: HwpUnit::ZERO, gap: if i < count - 1 { gap } else { HwpUnit::ZERO },
})
.collect();
Ok(Self {
column_type: ColumnType::Newspaper,
layout_mode: ColumnLayoutMode::Left,
columns,
col_line: None,
})
}
pub fn custom(columns: Vec<ColumnDef>) -> Result<Self, &'static str> {
if columns.len() < 2 {
return Err("column count must be >= 2 (use None for single column)");
}
Ok(Self {
column_type: ColumnType::Newspaper,
layout_mode: ColumnLayoutMode::Left,
columns,
col_line: None,
})
}
#[must_use]
pub fn with_separator(mut self, line: ColumnLine) -> Self {
self.col_line = Some(line);
self
}
pub fn count(&self) -> usize {
self.columns.len()
}
pub fn is_equal_width(&self) -> bool {
if self.columns.is_empty() {
return true;
}
let first = self.columns[0].width;
self.columns.iter().all(|c| c.width == first)
}
}
impl std::fmt::Display for ColumnSettings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ColumnSettings({} columns, {:?})", self.columns.len(), self.column_type)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equal_columns_2() {
let gap = HwpUnit::new(1134).unwrap();
let cs = ColumnSettings::equal_columns(2, gap).unwrap();
assert_eq!(cs.count(), 2);
assert_eq!(cs.column_type, ColumnType::Newspaper);
assert_eq!(cs.layout_mode, ColumnLayoutMode::Left);
assert_eq!(cs.columns[0].gap, gap);
assert_eq!(cs.columns[1].gap, HwpUnit::ZERO);
assert!(cs.is_equal_width());
}
#[test]
fn equal_columns_3() {
let gap = HwpUnit::new(1134).unwrap();
let cs = ColumnSettings::equal_columns(3, gap).unwrap();
assert_eq!(cs.count(), 3);
assert_eq!(cs.columns[0].gap, gap);
assert_eq!(cs.columns[1].gap, gap);
assert_eq!(cs.columns[2].gap, HwpUnit::ZERO);
}
#[test]
fn default_columns_have_no_separator() {
let cs = ColumnSettings::equal_columns(2, HwpUnit::new(1134).unwrap()).unwrap();
assert!(cs.col_line.is_none(), "default columns must have no separator line");
}
#[test]
fn column_line_default_is_owpml_default() {
let line = ColumnLine::default();
assert_eq!(line.line_type, BorderLineType::Solid);
assert_eq!(line.color, Color::from_rgb(0, 0, 0));
assert!((line.width.to_mm() - 0.12).abs() < 0.01);
}
#[test]
fn with_separator_attaches_col_line() {
let line = ColumnLine {
line_type: BorderLineType::DoubleSlim,
width: HwpUnit::from_mm(0.7).unwrap(),
color: Color::from_rgb(0x3A, 0x3C, 0x84),
};
let cs = ColumnSettings::equal_columns(2, HwpUnit::new(1134).unwrap())
.unwrap()
.with_separator(line);
assert_eq!(cs.col_line, Some(line));
}
#[test]
fn serde_round_trip_with_and_without_separator() {
let plain = ColumnSettings::equal_columns(2, HwpUnit::new(1134).unwrap()).unwrap();
let json = serde_json::to_string(&plain).unwrap();
assert!(!json.contains("col_line"), "absent separator must not serialize a key");
assert_eq!(serde_json::from_str::<ColumnSettings>(&json).unwrap(), plain);
let withed = plain.clone().with_separator(ColumnLine::default());
let json2 = serde_json::to_string(&withed).unwrap();
assert!(json2.contains("col_line"));
assert_eq!(serde_json::from_str::<ColumnSettings>(&json2).unwrap(), withed);
}
#[test]
fn equal_columns_returns_error_on_1() {
let result = ColumnSettings::equal_columns(1, HwpUnit::ZERO);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "column count must be >= 2 (use None for single column)");
}
#[test]
fn equal_columns_returns_error_on_0() {
let result = ColumnSettings::equal_columns(0, HwpUnit::ZERO);
assert!(result.is_err());
}
#[test]
fn custom_columns() {
let cs = ColumnSettings::custom(vec![
ColumnDef { width: HwpUnit::new(14000).unwrap(), gap: HwpUnit::new(1134).unwrap() },
ColumnDef { width: HwpUnit::new(27000).unwrap(), gap: HwpUnit::ZERO },
])
.unwrap();
assert_eq!(cs.count(), 2);
assert!(!cs.is_equal_width());
assert_eq!(cs.columns[0].width.as_i32(), 14000);
assert_eq!(cs.columns[1].width.as_i32(), 27000);
}
#[test]
fn custom_returns_error_on_1() {
let result =
ColumnSettings::custom(vec![ColumnDef { width: HwpUnit::ZERO, gap: HwpUnit::ZERO }]);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "column count must be >= 2 (use None for single column)");
}
#[test]
fn serde_roundtrip() {
let cs = ColumnSettings::equal_columns(2, HwpUnit::new(1134).unwrap()).unwrap();
let json = serde_json::to_string(&cs).unwrap();
let back: ColumnSettings = serde_json::from_str(&json).unwrap();
assert_eq!(cs, back);
}
#[test]
fn serde_roundtrip_custom() {
let cs = ColumnSettings::custom(vec![
ColumnDef { width: HwpUnit::new(14000).unwrap(), gap: HwpUnit::new(1134).unwrap() },
ColumnDef { width: HwpUnit::new(27000).unwrap(), gap: HwpUnit::ZERO },
])
.unwrap();
let json = serde_json::to_string(&cs).unwrap();
let back: ColumnSettings = serde_json::from_str(&json).unwrap();
assert_eq!(cs, back);
}
#[test]
fn display() {
let cs = ColumnSettings::equal_columns(2, HwpUnit::new(1134).unwrap()).unwrap();
let s = cs.to_string();
assert!(s.contains("2 columns"), "display: {s}");
assert!(s.contains("Newspaper"), "display: {s}");
}
#[test]
fn default_types() {
assert_eq!(ColumnType::default(), ColumnType::Newspaper);
assert_eq!(ColumnLayoutMode::default(), ColumnLayoutMode::Left);
}
#[test]
fn parallel_type() {
let mut cs = ColumnSettings::equal_columns(2, HwpUnit::ZERO).unwrap();
cs.column_type = ColumnType::Parallel;
assert_eq!(cs.column_type, ColumnType::Parallel);
}
#[test]
fn mirror_layout() {
let mut cs = ColumnSettings::equal_columns(2, HwpUnit::ZERO).unwrap();
cs.layout_mode = ColumnLayoutMode::Mirror;
assert_eq!(cs.layout_mode, ColumnLayoutMode::Mirror);
}
#[test]
fn is_equal_width_with_zero_widths() {
let cs = ColumnSettings::equal_columns(3, HwpUnit::new(1134).unwrap()).unwrap();
assert!(cs.is_equal_width());
}
#[test]
fn clone_independence() {
let cs = ColumnSettings::equal_columns(2, HwpUnit::new(1134).unwrap()).unwrap();
let mut cloned = cs.clone();
cloned.column_type = ColumnType::Parallel;
assert_eq!(cs.column_type, ColumnType::Newspaper);
assert_eq!(cloned.column_type, ColumnType::Parallel);
}
#[test]
fn column_settings_serde_roundtrip() {
let cs = ColumnSettings::equal_columns(2, HwpUnit::new(1134).unwrap()).unwrap();
let json = serde_json::to_string(&cs).unwrap();
let back: ColumnSettings = serde_json::from_str(&json).unwrap();
assert_eq!(cs, back);
}
}