use crate::accessibility;
use crate::prop_value::PropValue;
use crate::surface::SurfaceNode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonVariant {
Filled,
Outlined,
Text,
}
impl ButtonVariant {
fn as_str(self) -> &'static str {
match self {
Self::Filled => "filled",
Self::Outlined => "outlined",
Self::Text => "text",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyboardType {
Text,
Number,
Email,
Phone,
Url,
}
impl KeyboardType {
fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::Number => "number",
Self::Email => "email",
Self::Phone => "phone",
Self::Url => "url",
}
}
}
pub struct ButtonBuilder {
label: String,
on_tap: PropValue,
variant: Option<ButtonVariant>,
icon: Option<String>,
disabled: Option<bool>,
loading: Option<bool>,
}
impl ButtonBuilder {
pub fn new(label: impl Into<String>, on_tap: PropValue) -> Self {
Self {
label: label.into(),
on_tap,
variant: None,
icon: None,
disabled: None,
loading: None,
}
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = Some(variant);
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = Some(disabled);
self
}
pub fn loading(mut self, loading: bool) -> Self {
self.loading = Some(loading);
self
}
pub fn build(self) -> SurfaceNode {
let mut node = SurfaceNode::new("Button");
node.set_prop("label", PropValue::String(self.label));
node.set_prop("on_tap", self.on_tap);
if let Some(variant) = self.variant {
node.set_prop("variant", PropValue::String(variant.as_str().to_string()));
}
if let Some(icon) = self.icon {
node.set_prop("icon", PropValue::String(icon));
}
if let Some(disabled) = self.disabled {
node.set_prop("disabled", PropValue::Bool(disabled));
}
if let Some(loading) = self.loading {
node.set_prop("loading", PropValue::Bool(loading));
}
accessibility::ensure_accessible(&mut node);
node
}
}
pub struct TextInputBuilder {
value: String,
on_change: PropValue,
placeholder: Option<String>,
label: Option<String>,
keyboard: Option<KeyboardType>,
max_length: Option<f64>,
multiline: Option<bool>,
}
impl TextInputBuilder {
pub fn new(value: impl Into<String>, on_change: PropValue) -> Self {
Self {
value: value.into(),
on_change,
placeholder: None,
label: None,
keyboard: None,
max_length: None,
multiline: None,
}
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn keyboard(mut self, keyboard: KeyboardType) -> Self {
self.keyboard = Some(keyboard);
self
}
pub fn max_length(mut self, max_length: f64) -> Self {
self.max_length = Some(max_length);
self
}
pub fn multiline(mut self, multiline: bool) -> Self {
self.multiline = Some(multiline);
self
}
pub fn build(self) -> SurfaceNode {
let mut node = SurfaceNode::new("TextInput");
node.set_prop("value", PropValue::String(self.value));
node.set_prop("on_change", self.on_change);
if let Some(placeholder) = self.placeholder {
node.set_prop("placeholder", PropValue::String(placeholder));
}
if let Some(label) = self.label {
node.set_prop("label", PropValue::String(label));
}
if let Some(keyboard) = self.keyboard {
node.set_prop("keyboard", PropValue::String(keyboard.as_str().to_string()));
}
if let Some(max_length) = self.max_length {
node.set_prop("max_length", PropValue::Number(max_length));
}
if let Some(multiline) = self.multiline {
node.set_prop("multiline", PropValue::Bool(multiline));
}
accessibility::ensure_accessible(&mut node);
node
}
}
pub fn validate_interactive_node(node: &SurfaceNode) -> Vec<String> {
match node.component_type.as_str() {
"Button" => validate_button(node),
"TextInput" => validate_text_input(node),
_ => vec![format!(
"Unknown interactive component: {}",
node.component_type
)],
}
}
fn validate_button(node: &SurfaceNode) -> Vec<String> {
let mut errors = Vec::new();
match node.props.get("label") {
Some(PropValue::String(_)) => {}
Some(other) => errors.push(format!(
"Button.label: expected string, got {}",
other.type_name()
)),
None => errors.push("Button.label: required prop missing".to_string()),
}
match node.props.get("on_tap") {
Some(PropValue::ActionRef { .. }) => {}
Some(other) => errors.push(format!(
"Button.on_tap: expected action, got {}",
other.type_name()
)),
None => errors.push("Button.on_tap: required prop missing".to_string()),
}
if let Some(prop) = node.props.get("variant") {
match prop {
PropValue::String(s) if matches!(s.as_str(), "filled" | "outlined" | "text") => {}
_ => errors.push(format!(
"Button.variant: expected one of [filled, outlined, text], got {:?}",
prop
)),
}
}
if let Some(prop) = node.props.get("icon") {
if !matches!(prop, PropValue::String(_)) {
errors.push(format!(
"Button.icon: expected string, got {}",
prop.type_name()
));
}
}
if let Some(prop) = node.props.get("disabled") {
if !matches!(prop, PropValue::Bool(_)) {
errors.push(format!(
"Button.disabled: expected bool, got {}",
prop.type_name()
));
}
}
if let Some(prop) = node.props.get("loading") {
if !matches!(prop, PropValue::Bool(_)) {
errors.push(format!(
"Button.loading: expected bool, got {}",
prop.type_name()
));
}
}
if !node.children.is_empty() {
errors.push(format!(
"Button: does not accept children, but got {}",
node.children.len()
));
}
if let Some(prop) = node.props.get("accessible") {
errors.extend(accessibility::validate_accessible_prop("Button", prop));
}
for key in node.props.keys() {
if !matches!(
key.as_str(),
"label" | "on_tap" | "variant" | "icon" | "disabled" | "loading" | "accessible"
) {
errors.push(format!("Button: unknown prop '{key}'"));
}
}
errors
}
fn validate_text_input(node: &SurfaceNode) -> Vec<String> {
let mut errors = Vec::new();
match node.props.get("value") {
Some(PropValue::String(_)) => {}
Some(other) => errors.push(format!(
"TextInput.value: expected string, got {}",
other.type_name()
)),
None => errors.push("TextInput.value: required prop missing".to_string()),
}
match node.props.get("on_change") {
Some(PropValue::Lambda { .. }) => {}
Some(other) => errors.push(format!(
"TextInput.on_change: expected lambda, got {}",
other.type_name()
)),
None => errors.push("TextInput.on_change: required prop missing".to_string()),
}
if let Some(prop) = node.props.get("placeholder") {
if !matches!(prop, PropValue::String(_)) {
errors.push(format!(
"TextInput.placeholder: expected string, got {}",
prop.type_name()
));
}
}
if let Some(prop) = node.props.get("label") {
if !matches!(prop, PropValue::String(_)) {
errors.push(format!(
"TextInput.label: expected string, got {}",
prop.type_name()
));
}
}
if let Some(prop) = node.props.get("keyboard") {
match prop {
PropValue::String(s)
if matches!(s.as_str(), "text" | "number" | "email" | "phone" | "url") => {}
_ => errors.push(format!(
"TextInput.keyboard: expected one of [text, number, email, phone, url], got {:?}",
prop
)),
}
}
if let Some(prop) = node.props.get("max_length") {
if !matches!(prop, PropValue::Number(_)) {
errors.push(format!(
"TextInput.max_length: expected number, got {}",
prop.type_name()
));
}
}
if let Some(prop) = node.props.get("multiline") {
if !matches!(prop, PropValue::Bool(_)) {
errors.push(format!(
"TextInput.multiline: expected bool, got {}",
prop.type_name()
));
}
}
if !node.children.is_empty() {
errors.push(format!(
"TextInput: does not accept children, but got {}",
node.children.len()
));
}
if let Some(prop) = node.props.get("accessible") {
errors.extend(accessibility::validate_accessible_prop("TextInput", prop));
}
for key in node.props.keys() {
if !matches!(
key.as_str(),
"value"
| "on_change"
| "placeholder"
| "label"
| "keyboard"
| "max_length"
| "multiline"
| "accessible"
) {
errors.push(format!("TextInput: unknown prop '{key}'"));
}
}
errors
}