1use crate::accessibility;
7use crate::prop_value::PropValue;
8use crate::surface::SurfaceNode;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ButtonVariant {
15 Filled,
16 Outlined,
17 Text,
18}
19
20impl ButtonVariant {
21 fn as_str(self) -> &'static str {
22 match self {
23 Self::Filled => "filled",
24 Self::Outlined => "outlined",
25 Self::Text => "text",
26 }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum KeyboardType {
35 Text,
36 Number,
37 Email,
38 Phone,
39 Url,
40}
41
42impl KeyboardType {
43 fn as_str(self) -> &'static str {
44 match self {
45 Self::Text => "text",
46 Self::Number => "number",
47 Self::Email => "email",
48 Self::Phone => "phone",
49 Self::Url => "url",
50 }
51 }
52}
53
54pub struct ButtonBuilder {
61 label: String,
62 on_tap: PropValue,
63 variant: Option<ButtonVariant>,
64 icon: Option<String>,
65 disabled: Option<bool>,
66 loading: Option<bool>,
67}
68
69impl ButtonBuilder {
70 pub fn new(label: impl Into<String>, on_tap: PropValue) -> Self {
75 Self {
76 label: label.into(),
77 on_tap,
78 variant: None,
79 icon: None,
80 disabled: None,
81 loading: None,
82 }
83 }
84
85 pub fn variant(mut self, variant: ButtonVariant) -> Self {
86 self.variant = Some(variant);
87 self
88 }
89
90 pub fn icon(mut self, icon: impl Into<String>) -> Self {
91 self.icon = Some(icon.into());
92 self
93 }
94
95 pub fn disabled(mut self, disabled: bool) -> Self {
96 self.disabled = Some(disabled);
97 self
98 }
99
100 pub fn loading(mut self, loading: bool) -> Self {
101 self.loading = Some(loading);
102 self
103 }
104
105 pub fn build(self) -> SurfaceNode {
106 let mut node = SurfaceNode::new("Button");
107 node.set_prop("label", PropValue::String(self.label));
108 node.set_prop("on_tap", self.on_tap);
109 if let Some(variant) = self.variant {
110 node.set_prop("variant", PropValue::String(variant.as_str().to_string()));
111 }
112 if let Some(icon) = self.icon {
113 node.set_prop("icon", PropValue::String(icon));
114 }
115 if let Some(disabled) = self.disabled {
116 node.set_prop("disabled", PropValue::Bool(disabled));
117 }
118 if let Some(loading) = self.loading {
119 node.set_prop("loading", PropValue::Bool(loading));
120 }
121 accessibility::ensure_accessible(&mut node);
122 node
123 }
124}
125
126pub struct TextInputBuilder {
133 value: String,
134 on_change: PropValue,
135 placeholder: Option<String>,
136 label: Option<String>,
137 keyboard: Option<KeyboardType>,
138 max_length: Option<f64>,
139 multiline: Option<bool>,
140}
141
142impl TextInputBuilder {
143 pub fn new(value: impl Into<String>, on_change: PropValue) -> Self {
147 Self {
148 value: value.into(),
149 on_change,
150 placeholder: None,
151 label: None,
152 keyboard: None,
153 max_length: None,
154 multiline: None,
155 }
156 }
157
158 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
159 self.placeholder = Some(placeholder.into());
160 self
161 }
162
163 pub fn label(mut self, label: impl Into<String>) -> Self {
164 self.label = Some(label.into());
165 self
166 }
167
168 pub fn keyboard(mut self, keyboard: KeyboardType) -> Self {
169 self.keyboard = Some(keyboard);
170 self
171 }
172
173 pub fn max_length(mut self, max_length: f64) -> Self {
174 self.max_length = Some(max_length);
175 self
176 }
177
178 pub fn multiline(mut self, multiline: bool) -> Self {
179 self.multiline = Some(multiline);
180 self
181 }
182
183 pub fn build(self) -> SurfaceNode {
184 let mut node = SurfaceNode::new("TextInput");
185 node.set_prop("value", PropValue::String(self.value));
186 node.set_prop("on_change", self.on_change);
187 if let Some(placeholder) = self.placeholder {
188 node.set_prop("placeholder", PropValue::String(placeholder));
189 }
190 if let Some(label) = self.label {
191 node.set_prop("label", PropValue::String(label));
192 }
193 if let Some(keyboard) = self.keyboard {
194 node.set_prop("keyboard", PropValue::String(keyboard.as_str().to_string()));
195 }
196 if let Some(max_length) = self.max_length {
197 node.set_prop("max_length", PropValue::Number(max_length));
198 }
199 if let Some(multiline) = self.multiline {
200 node.set_prop("multiline", PropValue::Bool(multiline));
201 }
202 accessibility::ensure_accessible(&mut node);
203 node
204 }
205}
206
207pub fn validate_interactive_node(node: &SurfaceNode) -> Vec<String> {
211 match node.component_type.as_str() {
212 "Button" => validate_button(node),
213 "TextInput" => validate_text_input(node),
214 _ => vec![format!(
215 "Unknown interactive component: {}",
216 node.component_type
217 )],
218 }
219}
220
221fn validate_button(node: &SurfaceNode) -> Vec<String> {
222 let mut errors = Vec::new();
223
224 match node.props.get("label") {
226 Some(PropValue::String(_)) => {}
227 Some(other) => errors.push(format!(
228 "Button.label: expected string, got {}",
229 other.type_name()
230 )),
231 None => errors.push("Button.label: required prop missing".to_string()),
232 }
233
234 match node.props.get("on_tap") {
236 Some(PropValue::ActionRef { .. }) => {}
237 Some(other) => errors.push(format!(
238 "Button.on_tap: expected action, got {}",
239 other.type_name()
240 )),
241 None => errors.push("Button.on_tap: required prop missing".to_string()),
242 }
243
244 if let Some(prop) = node.props.get("variant") {
246 match prop {
247 PropValue::String(s) if matches!(s.as_str(), "filled" | "outlined" | "text") => {}
248 _ => errors.push(format!(
249 "Button.variant: expected one of [filled, outlined, text], got {:?}",
250 prop
251 )),
252 }
253 }
254
255 if let Some(prop) = node.props.get("icon") {
257 if !matches!(prop, PropValue::String(_)) {
258 errors.push(format!(
259 "Button.icon: expected string, got {}",
260 prop.type_name()
261 ));
262 }
263 }
264
265 if let Some(prop) = node.props.get("disabled") {
267 if !matches!(prop, PropValue::Bool(_)) {
268 errors.push(format!(
269 "Button.disabled: expected bool, got {}",
270 prop.type_name()
271 ));
272 }
273 }
274
275 if let Some(prop) = node.props.get("loading") {
277 if !matches!(prop, PropValue::Bool(_)) {
278 errors.push(format!(
279 "Button.loading: expected bool, got {}",
280 prop.type_name()
281 ));
282 }
283 }
284
285 if !node.children.is_empty() {
287 errors.push(format!(
288 "Button: does not accept children, but got {}",
289 node.children.len()
290 ));
291 }
292
293 if let Some(prop) = node.props.get("accessible") {
295 errors.extend(accessibility::validate_accessible_prop("Button", prop));
296 }
297
298 for key in node.props.keys() {
300 if !matches!(
301 key.as_str(),
302 "label" | "on_tap" | "variant" | "icon" | "disabled" | "loading" | "accessible"
303 ) {
304 errors.push(format!("Button: unknown prop '{key}'"));
305 }
306 }
307
308 errors
309}
310
311fn validate_text_input(node: &SurfaceNode) -> Vec<String> {
312 let mut errors = Vec::new();
313
314 match node.props.get("value") {
316 Some(PropValue::String(_)) => {}
317 Some(other) => errors.push(format!(
318 "TextInput.value: expected string, got {}",
319 other.type_name()
320 )),
321 None => errors.push("TextInput.value: required prop missing".to_string()),
322 }
323
324 match node.props.get("on_change") {
326 Some(PropValue::Lambda { .. }) => {}
327 Some(other) => errors.push(format!(
328 "TextInput.on_change: expected lambda, got {}",
329 other.type_name()
330 )),
331 None => errors.push("TextInput.on_change: required prop missing".to_string()),
332 }
333
334 if let Some(prop) = node.props.get("placeholder") {
336 if !matches!(prop, PropValue::String(_)) {
337 errors.push(format!(
338 "TextInput.placeholder: expected string, got {}",
339 prop.type_name()
340 ));
341 }
342 }
343
344 if let Some(prop) = node.props.get("label") {
346 if !matches!(prop, PropValue::String(_)) {
347 errors.push(format!(
348 "TextInput.label: expected string, got {}",
349 prop.type_name()
350 ));
351 }
352 }
353
354 if let Some(prop) = node.props.get("keyboard") {
356 match prop {
357 PropValue::String(s)
358 if matches!(s.as_str(), "text" | "number" | "email" | "phone" | "url") => {}
359 _ => errors.push(format!(
360 "TextInput.keyboard: expected one of [text, number, email, phone, url], got {:?}",
361 prop
362 )),
363 }
364 }
365
366 if let Some(prop) = node.props.get("max_length") {
368 if !matches!(prop, PropValue::Number(_)) {
369 errors.push(format!(
370 "TextInput.max_length: expected number, got {}",
371 prop.type_name()
372 ));
373 }
374 }
375
376 if let Some(prop) = node.props.get("multiline") {
378 if !matches!(prop, PropValue::Bool(_)) {
379 errors.push(format!(
380 "TextInput.multiline: expected bool, got {}",
381 prop.type_name()
382 ));
383 }
384 }
385
386 if !node.children.is_empty() {
388 errors.push(format!(
389 "TextInput: does not accept children, but got {}",
390 node.children.len()
391 ));
392 }
393
394 if let Some(prop) = node.props.get("accessible") {
396 errors.extend(accessibility::validate_accessible_prop("TextInput", prop));
397 }
398
399 for key in node.props.keys() {
401 if !matches!(
402 key.as_str(),
403 "value"
404 | "on_change"
405 | "placeholder"
406 | "label"
407 | "keyboard"
408 | "max_length"
409 | "multiline"
410 | "accessible"
411 ) {
412 errors.push(format!("TextInput: unknown prop '{key}'"));
413 }
414 }
415
416 errors
417}