cdx_core/presentation/
layout.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct ColumnLayout {
9 pub columns: u32,
11
12 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub gap: Option<String>,
15
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub balance: Option<bool>,
19}
20
21impl ColumnLayout {
22 #[must_use]
24 pub const fn new(columns: u32) -> Self {
25 Self {
26 columns,
27 gap: None,
28 balance: None,
29 }
30 }
31
32 #[must_use]
34 pub fn with_gap(mut self, gap: impl Into<String>) -> Self {
35 self.gap = Some(gap.into());
36 self
37 }
38
39 #[must_use]
41 pub const fn balanced(mut self) -> Self {
42 self.balance = Some(true);
43 self
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct GridLayout {
51 pub columns: String,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub rows: Option<String>,
57
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub gap: Option<String>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub areas: Option<Vec<GridArea>>,
65}
66
67impl GridLayout {
68 #[must_use]
70 pub fn new(columns: impl Into<String>) -> Self {
71 Self {
72 columns: columns.into(),
73 rows: None,
74 gap: None,
75 areas: None,
76 }
77 }
78
79 #[must_use]
81 pub fn with_rows(mut self, rows: impl Into<String>) -> Self {
82 self.rows = Some(rows.into());
83 self
84 }
85
86 #[must_use]
88 pub fn with_gap(mut self, gap: impl Into<String>) -> Self {
89 self.gap = Some(gap.into());
90 self
91 }
92
93 #[must_use]
95 pub fn with_areas(mut self, areas: Vec<GridArea>) -> Self {
96 self.areas = Some(areas);
97 self
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct GridArea {
105 pub name: String,
107
108 pub column: String,
110
111 pub row: String,
113}
114
115impl GridArea {
116 #[must_use]
118 pub fn new(name: impl Into<String>, column: impl Into<String>, row: impl Into<String>) -> Self {
119 Self {
120 name: name.into(),
121 column: column.into(),
122 row: row.into(),
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_column_layout_serde() {
133 let layout = ColumnLayout::new(2).with_gap("20px").balanced();
134 let json = serde_json::to_string_pretty(&layout).unwrap();
135 assert!(json.contains("\"columns\": 2"));
136 assert!(json.contains("\"gap\": \"20px\""));
137 assert!(json.contains("\"balance\": true"));
138
139 let parsed: ColumnLayout = serde_json::from_str(&json).unwrap();
140 assert_eq!(parsed, layout);
141 }
142
143 #[test]
144 fn test_grid_layout_serde() {
145 let layout = GridLayout::new("1fr 2fr")
146 .with_rows("auto 1fr")
147 .with_gap("10px");
148
149 let json = serde_json::to_string(&layout).unwrap();
150 assert!(json.contains("\"columns\":\"1fr 2fr\""));
151
152 let parsed: GridLayout = serde_json::from_str(&json).unwrap();
153 assert_eq!(parsed, layout);
154 }
155
156 #[test]
157 fn test_grid_area_serde() {
158 let area = GridArea::new("header", "1 / 13", "1 / 2");
159 let json = serde_json::to_string(&area).unwrap();
160 assert!(json.contains("\"name\":\"header\""));
161 assert!(json.contains("\"column\":\"1 / 13\""));
162
163 let parsed: GridArea = serde_json::from_str(&json).unwrap();
164 assert_eq!(parsed, area);
165 }
166
167 #[test]
168 fn test_grid_with_areas() {
169 let layout = GridLayout::new("repeat(12, 1fr)").with_areas(vec![
170 GridArea::new("header", "1 / 13", "1 / 2"),
171 GridArea::new("sidebar", "1 / 4", "2 / 3"),
172 GridArea::new("content", "4 / 13", "2 / 3"),
173 ]);
174
175 let json = serde_json::to_string_pretty(&layout).unwrap();
176 let parsed: GridLayout = serde_json::from_str(&json).unwrap();
177 assert_eq!(parsed, layout);
178 assert_eq!(parsed.areas.unwrap().len(), 3);
179 }
180
181 #[test]
182 fn test_column_layout_defaults() {
183 let json = r#"{"columns": 3}"#;
184 let layout: ColumnLayout = serde_json::from_str(json).unwrap();
185 assert_eq!(layout.columns, 3);
186 assert!(layout.gap.is_none());
187 assert!(layout.balance.is_none());
188 }
189}