extends VBoxContainer class_name FormField ## Form Field Component ## ## Combines label, input field, and error message display. ## Supports validation and different input types. ## ## Structure: ## VBoxContainer ## ├─ Label (field label) ## ├─ LineEdit (input field) ## └─ Label (error message, hidden by default) ## ## Usage: ## var field = FormField.new() ## field.label_text = "Email" ## field.placeholder = "Enter your email" ## field.input_type = FormField.InputType.EMAIL ## field.value_changed.connect(_on_email_changed) signal value_changed(new_value: String) signal validation_changed(is_valid: bool) # Input types enum InputType { TEXT, # Plain text EMAIL, # Email validation PASSWORD, # Password (hidden) NUMBER, # Numeric only PHONE, # Phone number } # Export variables @export var label_text: String = "" @export var placeholder: String = "" @export var input_type: InputType = InputType.TEXT @export var required: bool = false @export var min_length: int = 0 @export var max_length: int = 0 # Internal nodes var _label: Label = null var _input: LineEdit = null var _error_label: Label = null # State var _is_valid: bool = true var _error_message: String = "" func _ready() -> void: _setup_structure() _apply_styling() ## Setup form field structure func _setup_structure() -> void: # Label _label = Label.new() _label.text = label_text add_child(_label) # Spacing var spacer1 = Control.new() spacer1.custom_minimum_size = Vector2(0, 4) add_child(spacer1) # Input field _input = LineEdit.new() _input.placeholder_text = placeholder _input.text_changed.connect(_on_text_changed) _input.text_submitted.connect(_on_text_submitted) # Set input type match input_type: InputType.PASSWORD: _input.secret = true InputType.NUMBER: # TODO: Add number-only filter pass _: pass # Set max length if specified if max_length > 0: _input.max_length = max_length add_child(_input) # Spacing var spacer2 = Control.new() spacer2.custom_minimum_size = Vector2(0, 2) add_child(spacer2) # Error label _error_label = Label.new() _error_label.visible = false add_child(_error_label) ## Apply styling func _apply_styling() -> void: # Label styling _label.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY) # Input styling var normal_style = StyleBoxFlat.new() normal_style.bg_color = ThemeColors.BACKGROUND_SECONDARY normal_style.border_width_all = 2 normal_style.border_color = ThemeColors.BORDER_DEFAULT normal_style.corner_radius_all = 4 normal_style.content_margin_left = 12 normal_style.content_margin_right = 12 normal_style.content_margin_top = 8 normal_style.content_margin_bottom = 8 _input.add_theme_stylebox_override("normal", normal_style) var focus_style = normal_style.duplicate() focus_style.border_color = ThemeColors.GOLD_ACCENT _input.add_theme_stylebox_override("focus", focus_style) _input.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY) _input.add_theme_color_override("font_placeholder_color", ThemeColors.TEXT_SECONDARY) _input.add_theme_color_override("caret_color", ThemeColors.GOLD_ACCENT) # Error label styling _error_label.add_theme_color_override("font_color", ThemeColors.ERROR) # TODO: Add smaller font size when theme is set up ## Get current value func get_value() -> String: return _input.text if _input else "" ## Set value func set_value(value: String) -> void: if _input: _input.text = value _validate() ## Set label text func set_label(text: String) -> void: label_text = text if _label: _label.text = text ## Set placeholder text func set_placeholder(text: String) -> void: placeholder = text if _input: _input.placeholder_text = text ## Show error message func show_error(message: String) -> void: _error_message = message _is_valid = false if _error_label: _error_label.text = message _error_label.visible = true # Change input border to error color if _input: var error_style = StyleBoxFlat.new() error_style.bg_color = ThemeColors.BACKGROUND_SECONDARY error_style.border_width_all = 2 error_style.border_color = ThemeColors.ERROR error_style.corner_radius_all = 4 error_style.content_margin_left = 12 error_style.content_margin_right = 12 error_style.content_margin_top = 8 error_style.content_margin_bottom = 8 _input.add_theme_stylebox_override("normal", error_style) validation_changed.emit(false) ## Clear error func clear_error() -> void: _error_message = "" _is_valid = true if _error_label: _error_label.visible = false # Restore normal input styling _apply_styling() validation_changed.emit(true) ## Check if field is valid func is_valid() -> bool: return _is_valid ## Focus the input field func focus_input() -> void: if _input: _input.grab_focus() ## Validate the field func validate() -> bool: return _validate() ## Internal: Validate input func _validate() -> bool: var value = get_value() # Required check if required and value.is_empty(): show_error("This field is required") return false # Min length check if min_length > 0 and value.length() < min_length: show_error("Must be at least %d characters" % min_length) return false # Max length check (redundant with LineEdit.max_length, but kept for consistency) if max_length > 0 and value.length() > max_length: show_error("Must be no more than %d characters" % max_length) return false # Type-specific validation match input_type: InputType.EMAIL: if not _is_valid_email(value) and not value.is_empty(): show_error("Invalid email address") return false InputType.NUMBER: if not value.is_valid_int() and not value.is_empty(): show_error("Must be a number") return false InputType.PHONE: if not _is_valid_phone(value) and not value.is_empty(): show_error("Invalid phone number") return false # All validations passed clear_error() return true ## Internal: Email validation func _is_valid_email(email: String) -> bool: # Basic email regex validation var regex = RegEx.new() regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") return regex.search(email) != null ## Internal: Phone validation func _is_valid_phone(phone: String) -> bool: # Basic phone validation (digits, spaces, dashes, parentheses) var regex = RegEx.new() regex.compile("^[0-9\\s\\-\\(\\)\\+]+$") return regex.search(phone) != null and phone.length() >= 10 ## Internal: Handle text change func _on_text_changed(new_text: String) -> void: # Clear error when user starts typing if not _error_message.is_empty(): clear_error() value_changed.emit(new_text) ## Internal: Handle text submission (Enter key) func _on_text_submitted(new_text: String) -> void: _validate()