1use crate::accessibility;
7use crate::prop_value::PropValue;
8use crate::surface::SurfaceNode;
9use crate::types::ColorValue;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum TextSize {
16 Small,
17 Body,
18 Title,
19 Heading,
20 Display,
21}
22
23impl TextSize {
24 fn as_str(self) -> &'static str {
25 match self {
26 Self::Small => "small",
27 Self::Body => "body",
28 Self::Title => "title",
29 Self::Heading => "heading",
30 Self::Display => "display",
31 }
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum TextWeight {
40 Normal,
41 Medium,
42 Bold,
43}
44
45impl TextWeight {
46 fn as_str(self) -> &'static str {
47 match self {
48 Self::Normal => "normal",
49 Self::Medium => "medium",
50 Self::Bold => "bold",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum TextAlign {
60 Start,
61 Center,
62 End,
63}
64
65impl TextAlign {
66 fn as_str(self) -> &'static str {
67 match self {
68 Self::Start => "start",
69 Self::Center => "center",
70 Self::End => "end",
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
79pub enum TextOverflow {
80 Clip,
81 Ellipsis,
82 Wrap,
83}
84
85impl TextOverflow {
86 fn as_str(self) -> &'static str {
87 match self {
88 Self::Clip => "clip",
89 Self::Ellipsis => "ellipsis",
90 Self::Wrap => "wrap",
91 }
92 }
93}
94
95pub struct TextBuilder {
115 value: String,
116 size: Option<TextSize>,
117 weight: Option<TextWeight>,
118 color: Option<ColorValue>,
119 align: Option<TextAlign>,
120 max_lines: Option<f64>,
121 overflow: Option<TextOverflow>,
122}
123
124impl TextBuilder {
125 pub fn new(value: impl Into<String>) -> Self {
127 Self {
128 value: value.into(),
129 size: None,
130 weight: None,
131 color: None,
132 align: None,
133 max_lines: None,
134 overflow: None,
135 }
136 }
137
138 pub fn size(mut self, size: TextSize) -> Self {
140 self.size = Some(size);
141 self
142 }
143
144 pub fn weight(mut self, weight: TextWeight) -> Self {
146 self.weight = Some(weight);
147 self
148 }
149
150 pub fn color(mut self, color: ColorValue) -> Self {
152 self.color = Some(color);
153 self
154 }
155
156 pub fn align(mut self, align: TextAlign) -> Self {
158 self.align = Some(align);
159 self
160 }
161
162 pub fn max_lines(mut self, max_lines: f64) -> Self {
164 self.max_lines = Some(max_lines);
165 self
166 }
167
168 pub fn overflow(mut self, overflow: TextOverflow) -> Self {
170 self.overflow = Some(overflow);
171 self
172 }
173
174 pub fn build(self) -> SurfaceNode {
176 let mut node = SurfaceNode::new("Text");
177 node.set_prop("value", PropValue::String(self.value));
178 if let Some(size) = self.size {
179 node.set_prop("size", PropValue::String(size.as_str().to_string()));
180 }
181 if let Some(weight) = self.weight {
182 node.set_prop("weight", PropValue::String(weight.as_str().to_string()));
183 }
184 if let Some(color) = self.color {
185 node.set_prop(
186 "color",
187 PropValue::color(color.r, color.g, color.b, color.a),
188 );
189 }
190 if let Some(align) = self.align {
191 node.set_prop("align", PropValue::String(align.as_str().to_string()));
192 }
193 if let Some(max_lines) = self.max_lines {
194 node.set_prop("max_lines", PropValue::Number(max_lines));
195 }
196 if let Some(overflow) = self.overflow {
197 node.set_prop("overflow", PropValue::String(overflow.as_str().to_string()));
198 }
199 accessibility::ensure_accessible(&mut node);
200 node
201 }
202}
203
204pub struct ProgressBarBuilder {
219 value: f64,
220 color: Option<ColorValue>,
221 background: Option<ColorValue>,
222 height: Option<f64>,
223}
224
225impl ProgressBarBuilder {
226 pub fn new(value: f64) -> Self {
230 Self {
231 value: value.clamp(0.0, 1.0),
232 color: None,
233 background: None,
234 height: None,
235 }
236 }
237
238 pub fn color(mut self, color: ColorValue) -> Self {
240 self.color = Some(color);
241 self
242 }
243
244 pub fn background(mut self, background: ColorValue) -> Self {
246 self.background = Some(background);
247 self
248 }
249
250 pub fn height(mut self, height: f64) -> Self {
252 self.height = Some(height);
253 self
254 }
255
256 pub fn build(self) -> SurfaceNode {
258 let mut node = SurfaceNode::new("ProgressBar");
259 node.set_prop("value", PropValue::Number(self.value));
260 if let Some(color) = self.color {
261 node.set_prop(
262 "color",
263 PropValue::color(color.r, color.g, color.b, color.a),
264 );
265 }
266 if let Some(background) = self.background {
267 node.set_prop(
268 "background",
269 PropValue::color(background.r, background.g, background.b, background.a),
270 );
271 }
272 if let Some(height) = self.height {
273 node.set_prop("height", PropValue::Number(height));
274 }
275 accessibility::ensure_accessible(&mut node);
276 node
277 }
278}
279
280pub fn validate_content_node(node: &SurfaceNode) -> Vec<String> {
287 match node.component_type.as_str() {
288 "Text" => validate_text(node),
289 "ProgressBar" => validate_progress_bar(node),
290 _ => vec![format!(
291 "Unknown content component: {}",
292 node.component_type
293 )],
294 }
295}
296
297fn validate_text(node: &SurfaceNode) -> Vec<String> {
298 let mut errors = Vec::new();
299
300 match node.props.get("value") {
302 Some(PropValue::String(_)) => {}
303 Some(other) => errors.push(format!(
304 "Text.value: expected string, got {}",
305 other.type_name()
306 )),
307 None => errors.push("Text.value: required prop missing".to_string()),
308 }
309
310 if let Some(prop) = node.props.get("size") {
312 match prop {
313 PropValue::String(s)
314 if matches!(
315 s.as_str(),
316 "small" | "body" | "title" | "heading" | "display"
317 ) => {}
318 _ => errors.push(format!(
319 "Text.size: expected one of [small, body, title, heading, display], got {:?}",
320 prop
321 )),
322 }
323 }
324
325 if let Some(prop) = node.props.get("weight") {
327 match prop {
328 PropValue::String(s) if matches!(s.as_str(), "normal" | "medium" | "bold") => {}
329 _ => errors.push(format!(
330 "Text.weight: expected one of [normal, medium, bold], got {:?}",
331 prop
332 )),
333 }
334 }
335
336 if let Some(prop) = node.props.get("color") {
338 if !matches!(prop, PropValue::Color { .. }) {
339 errors.push(format!(
340 "Text.color: expected color, got {}",
341 prop.type_name()
342 ));
343 }
344 }
345
346 if let Some(prop) = node.props.get("align") {
348 match prop {
349 PropValue::String(s) if matches!(s.as_str(), "start" | "center" | "end") => {}
350 _ => errors.push(format!(
351 "Text.align: expected one of [start, center, end], got {:?}",
352 prop
353 )),
354 }
355 }
356
357 if let Some(prop) = node.props.get("max_lines") {
359 if !matches!(prop, PropValue::Number(_)) {
360 errors.push(format!(
361 "Text.max_lines: expected number, got {}",
362 prop.type_name()
363 ));
364 }
365 }
366
367 if let Some(prop) = node.props.get("overflow") {
369 match prop {
370 PropValue::String(s) if matches!(s.as_str(), "clip" | "ellipsis" | "wrap") => {}
371 _ => errors.push(format!(
372 "Text.overflow: expected one of [clip, ellipsis, wrap], got {:?}",
373 prop
374 )),
375 }
376 }
377
378 if !node.children.is_empty() {
380 errors.push(format!(
381 "Text: does not accept children, but got {}",
382 node.children.len()
383 ));
384 }
385
386 if let Some(prop) = node.props.get("accessible") {
388 errors.extend(accessibility::validate_accessible_prop("Text", prop));
389 }
390
391 for key in node.props.keys() {
393 if !matches!(
394 key.as_str(),
395 "value"
396 | "size"
397 | "weight"
398 | "color"
399 | "align"
400 | "max_lines"
401 | "overflow"
402 | "accessible"
403 ) {
404 errors.push(format!("Text: unknown prop '{key}'"));
405 }
406 }
407
408 errors
409}
410
411fn validate_progress_bar(node: &SurfaceNode) -> Vec<String> {
412 let mut errors = Vec::new();
413
414 match node.props.get("value") {
416 Some(PropValue::Number(_)) => {}
417 Some(other) => errors.push(format!(
418 "ProgressBar.value: expected number, got {}",
419 other.type_name()
420 )),
421 None => errors.push("ProgressBar.value: required prop missing".to_string()),
422 }
423
424 if let Some(prop) = node.props.get("color") {
426 if !matches!(prop, PropValue::Color { .. }) {
427 errors.push(format!(
428 "ProgressBar.color: expected color, got {}",
429 prop.type_name()
430 ));
431 }
432 }
433
434 if let Some(prop) = node.props.get("background") {
436 if !matches!(prop, PropValue::Color { .. }) {
437 errors.push(format!(
438 "ProgressBar.background: expected color, got {}",
439 prop.type_name()
440 ));
441 }
442 }
443
444 if let Some(prop) = node.props.get("height") {
446 if !matches!(prop, PropValue::Number(_)) {
447 errors.push(format!(
448 "ProgressBar.height: expected number, got {}",
449 prop.type_name()
450 ));
451 }
452 }
453
454 if !node.children.is_empty() {
456 errors.push(format!(
457 "ProgressBar: does not accept children, but got {}",
458 node.children.len()
459 ));
460 }
461
462 if let Some(prop) = node.props.get("accessible") {
464 errors.extend(accessibility::validate_accessible_prop("ProgressBar", prop));
465 }
466
467 for key in node.props.keys() {
469 if !matches!(
470 key.as_str(),
471 "value" | "color" | "background" | "height" | "accessible"
472 ) {
473 errors.push(format!("ProgressBar: unknown prop '{key}'"));
474 }
475 }
476
477 errors
478}