1use crate::accessibility;
16use crate::prop_value::PropValue;
17use crate::surface::SurfaceNode;
18use crate::types::{Alignment, Edges};
19use serde_json;
20
21pub struct ColumnBuilder {
32 spacing: Option<f64>,
33 align: Option<Alignment>,
34 padding: Option<Edges>,
35 children: Vec<SurfaceNode>,
36}
37
38impl ColumnBuilder {
39 pub fn new() -> Self {
40 Self {
41 spacing: None,
42 align: None,
43 padding: None,
44 children: Vec::new(),
45 }
46 }
47
48 pub fn spacing(mut self, spacing: f64) -> Self {
49 self.spacing = Some(spacing);
50 self
51 }
52
53 pub fn align(mut self, align: Alignment) -> Self {
54 self.align = Some(align);
55 self
56 }
57
58 pub fn padding(mut self, padding: Edges) -> Self {
59 self.padding = Some(padding);
60 self
61 }
62
63 pub fn child(mut self, child: SurfaceNode) -> Self {
64 self.children.push(child);
65 self
66 }
67
68 pub fn children(mut self, children: Vec<SurfaceNode>) -> Self {
69 self.children = children;
70 self
71 }
72
73 pub fn build(self) -> SurfaceNode {
74 let mut node = SurfaceNode::new("Column");
75
76 if let Some(spacing) = self.spacing {
77 node.set_prop("spacing", PropValue::Number(spacing));
78 }
79 if let Some(align) = self.align {
80 node.set_prop("align", alignment_to_prop(align));
81 }
82 if let Some(padding) = self.padding {
83 node.set_prop("padding", edges_to_prop(padding));
84 }
85
86 node.children = self.children;
87 accessibility::ensure_accessible(&mut node);
88 node
89 }
90}
91
92impl Default for ColumnBuilder {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98pub struct RowBuilder {
104 spacing: Option<f64>,
105 align: Option<Alignment>,
106 padding: Option<Edges>,
107 children: Vec<SurfaceNode>,
108}
109
110impl RowBuilder {
111 pub fn new() -> Self {
112 Self {
113 spacing: None,
114 align: None,
115 padding: None,
116 children: Vec::new(),
117 }
118 }
119
120 pub fn spacing(mut self, spacing: f64) -> Self {
121 self.spacing = Some(spacing);
122 self
123 }
124
125 pub fn align(mut self, align: Alignment) -> Self {
126 self.align = Some(align);
127 self
128 }
129
130 pub fn padding(mut self, padding: Edges) -> Self {
131 self.padding = Some(padding);
132 self
133 }
134
135 pub fn child(mut self, child: SurfaceNode) -> Self {
136 self.children.push(child);
137 self
138 }
139
140 pub fn children(mut self, children: Vec<SurfaceNode>) -> Self {
141 self.children = children;
142 self
143 }
144
145 pub fn build(self) -> SurfaceNode {
146 let mut node = SurfaceNode::new("Row");
147
148 if let Some(spacing) = self.spacing {
149 node.set_prop("spacing", PropValue::Number(spacing));
150 }
151 if let Some(align) = self.align {
152 node.set_prop("align", alignment_to_prop(align));
153 }
154 if let Some(padding) = self.padding {
155 node.set_prop("padding", edges_to_prop(padding));
156 }
157
158 node.children = self.children;
159 accessibility::ensure_accessible(&mut node);
160 node
161 }
162}
163
164impl Default for RowBuilder {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub enum ScrollDirection {
175 #[default]
176 Vertical,
177 Horizontal,
178 Both,
179}
180
181impl ScrollDirection {
182 pub fn as_str(&self) -> &'static str {
184 match self {
185 ScrollDirection::Vertical => "vertical",
186 ScrollDirection::Horizontal => "horizontal",
187 ScrollDirection::Both => "both",
188 }
189 }
190}
191
192pub struct ScrollBuilder {
196 direction: ScrollDirection,
197 children: Vec<SurfaceNode>,
198}
199
200impl ScrollBuilder {
201 pub fn new() -> Self {
202 Self {
203 direction: ScrollDirection::default(),
204 children: Vec::new(),
205 }
206 }
207
208 pub fn direction(mut self, direction: ScrollDirection) -> Self {
209 self.direction = direction;
210 self
211 }
212
213 pub fn child(mut self, child: SurfaceNode) -> Self {
214 self.children.push(child);
215 self
216 }
217
218 pub fn children(mut self, children: Vec<SurfaceNode>) -> Self {
219 self.children = children;
220 self
221 }
222
223 pub fn build(self) -> SurfaceNode {
224 let mut node = SurfaceNode::new("Scroll");
225 node.set_prop(
226 "direction",
227 PropValue::String(self.direction.as_str().to_string()),
228 );
229 node.children = self.children;
230 accessibility::ensure_accessible(&mut node);
231 node
232 }
233}
234
235impl Default for ScrollBuilder {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241fn alignment_to_prop(align: Alignment) -> PropValue {
245 let s = match align {
246 Alignment::Start => "start",
247 Alignment::Center => "center",
248 Alignment::End => "end",
249 Alignment::Stretch => "stretch",
250 Alignment::SpaceBetween => "space_between",
251 Alignment::SpaceAround => "space_around",
252 };
253 PropValue::String(s.to_string())
254}
255
256fn edges_to_prop(edges: Edges) -> PropValue {
261 match edges {
262 Edges::Uniform(n) => PropValue::Number(n),
263 Edges::Sides { .. } => {
264 let s = serde_json::to_value(&edges).expect("Edges serialization should never fail");
265 serde_json::from_value(s).expect("Edges deserialization should never fail")
266 }
267 }
268}
269
270pub fn validate_layout_node(node: &SurfaceNode) -> Vec<String> {
274 let mut errors = Vec::new();
275
276 match node.component_type.as_str() {
277 "Column" | "Row" => {
278 for (key, val) in &node.props {
279 match key.as_str() {
280 "spacing" => {
281 if !matches!(val, PropValue::Number(_)) {
282 errors.push(format!(
283 "{}: 'spacing' must be a number, got {}",
284 node.component_type,
285 val.type_name()
286 ));
287 }
288 }
289 "align" => {
290 if let PropValue::String(s) = val {
291 let valid = [
292 "start",
293 "center",
294 "end",
295 "stretch",
296 "space_between",
297 "space_around",
298 ];
299 if !valid.contains(&s.as_str()) {
300 errors.push(format!(
301 "{}: invalid alignment '{s}'",
302 node.component_type
303 ));
304 }
305 } else {
306 errors.push(format!(
307 "{}: 'align' must be a string, got {}",
308 node.component_type,
309 val.type_name()
310 ));
311 }
312 }
313 "padding" => {
314 if !matches!(val, PropValue::Number(_) | PropValue::Record(_)) {
316 errors.push(format!(
317 "{}: 'padding' must be a number or record, got {}",
318 node.component_type,
319 val.type_name()
320 ));
321 }
322 }
323 "accessible" => {
324 errors.extend(accessibility::validate_accessible_prop(
325 &node.component_type,
326 val,
327 ));
328 }
329 other => {
330 errors.push(format!("{}: unknown prop '{other}'", node.component_type));
331 }
332 }
333 }
334 }
335 "Scroll" => {
336 for (key, val) in &node.props {
337 match key.as_str() {
338 "direction" => {
339 if let PropValue::String(s) = val {
340 let valid = ["vertical", "horizontal", "both"];
341 if !valid.contains(&s.as_str()) {
342 errors.push(format!("Scroll: invalid direction '{s}'"));
343 }
344 } else {
345 errors.push(format!(
346 "Scroll: 'direction' must be a string, got {}",
347 val.type_name()
348 ));
349 }
350 }
351 "accessible" => {
352 errors.extend(accessibility::validate_accessible_prop("Scroll", val));
353 }
354 other => {
355 errors.push(format!("Scroll: unknown prop '{other}'"));
356 }
357 }
358 }
359 }
360 _ => {} }
362
363 errors
364}