7.4 KiB
7.4 KiB
Reusable UI Components
This directory contains reusable UI components for Code of Conquest.
Available Components
CustomButton
Script: scripts/components/custom_button.gd
Enhanced button with multiple visual variants and features.
Features:
- Multiple variants (Primary, Secondary, Danger, Success, Ghost)
- Icon support (left or right position)
- Loading state
- Hover effects
- Themed styling
Usage:
var btn = CustomButton.new()
btn.text = "Login"
btn.set_variant(CustomButton.Variant.PRIMARY)
btn.button_clicked.connect(_on_login_clicked)
add_child(btn)
Variants:
PRIMARY- Gold accent button for main actionsSECONDARY- Standard buttonDANGER- Red button for destructive actions (delete, etc.)SUCCESS- Green button for positive actionsGHOST- Transparent/subtle button
Card
Script: scripts/components/card.gd
Container with optional header and footer, styled like the web UI cards.
Features:
- Optional header with text
- Body content area
- Optional footer with buttons
- Multiple style variants
- Automatic sizing
Usage:
var card = Card.new()
card.set_header("Character Details")
var content = Label.new()
content.text = "Character information goes here"
card.add_content(content)
card.set_footer_buttons(["Save", "Cancel"])
card.footer_button_pressed.connect(_on_card_button_pressed)
add_child(card)
Style Variants:
DEFAULT- Standard card with subtle borderHIGHLIGHTED- Gold border with shadowSUBTLE- Minimal border
FormField
Script: scripts/components/form_field.gd
Form input field with label, validation, and error display.
Features:
- Label + input + error message
- Multiple input types (text, email, password, number, phone)
- Built-in validation
- Required field support
- Min/max length validation
- Error state styling
Usage:
var email_field = FormField.new()
email_field.set_label("Email")
email_field.set_placeholder("Enter your email")
email_field.input_type = FormField.InputType.EMAIL
email_field.required = true
email_field.value_changed.connect(_on_email_changed)
add_child(email_field)
# Later, validate
if email_field.validate():
var email = email_field.get_value()
# Process email
Input Types:
TEXT- Plain textEMAIL- Email validationPASSWORD- Hidden passwordNUMBER- Numeric onlyPHONE- Phone number format
Creating New Components
1. Create Script
Create a new GDScript file in scripts/components/:
extends Control # or appropriate base class
class_name MyComponent
## MyComponent
##
## Brief description of what this component does
##
## Usage:
## var comp = MyComponent.new()
## comp.some_property = "value"
signal some_signal(data: String)
@export var some_property: String = ""
func _ready() -> void:
_setup_structure()
_apply_styling()
func _setup_structure() -> void:
# Build node hierarchy
pass
func _apply_styling() -> void:
# Apply theme colors and styleboxes
pass
2. Use Theme Colors
Always use ThemeColors constants:
var style = StyleBoxFlat.new()
style.bg_color = ThemeColors.BACKGROUND_CARD
style.border_color = ThemeColors.BORDER_DEFAULT
3. Make It Configurable
Use @export variables for Godot Inspector:
@export var title: String = ""
@export_enum("Small", "Medium", "Large") var size: String = "Medium"
@export var show_icon: bool = true
4. Emit Signals
For user interactions:
signal item_selected(item_id: String)
signal value_changed(new_value: Variant)
func _on_internal_action():
item_selected.emit("some_id")
5. Document Usage
Include usage examples in docstring:
## MyComponent
##
## Detailed description of the component.
##
## Usage:
## var comp = MyComponent.new()
## comp.title = "My Title"
## comp.item_selected.connect(_on_item_selected)
## add_child(comp)
Component Patterns
Separation of Structure and Style
func _ready() -> void:
_setup_structure() # Build node hierarchy
_apply_styling() # Apply theme
func _setup_structure() -> void:
# Create child nodes
# Set up hierarchy
# Connect signals
pass
func _apply_styling() -> void:
# Apply StyleBoxes
# Set colors
# Set fonts
pass
Responsive Design
func _apply_styling() -> void:
# Check platform
var is_mobile = OS.get_name() in ["Android", "iOS"]
if is_mobile:
# Larger touch targets
custom_minimum_size = Vector2(60, 60)
else:
# Desktop sizing
custom_minimum_size = Vector2(40, 40)
Validation Pattern
func validate() -> bool:
var is_valid = true
# Check conditions
if some_condition:
show_error("Error message")
is_valid = false
if is_valid:
clear_error()
return is_valid
Testing Components
Create a test scene test_components.tscn:
- Add each component
- Test all variants
- Test all states (normal, hover, pressed, disabled)
- Test on different screen sizes
- Test on different platforms
Example test scene structure:
Control (root)
├─ VBoxContainer
│ ├─ Label ("Buttons")
│ ├─ HBoxContainer
│ │ ├─ CustomButton (Primary)
│ │ ├─ CustomButton (Secondary)
│ │ ├─ CustomButton (Danger)
│ │ └─ CustomButton (Ghost)
│ ├─ Label ("Cards")
│ ├─ HBoxContainer
│ │ ├─ Card (Default)
│ │ ├─ Card (Highlighted)
│ │ └─ Card (Subtle)
│ └─ Label ("Form Fields")
│ ├─ FormField (Text)
│ ├─ FormField (Email)
│ └─ FormField (Password)
Best Practices
- Always use ThemeColors - Never hardcode colors
- Make components reusable - Avoid scene-specific logic
- Use signals for communication - Don't couple components
- Document everything - Docstrings + usage examples
- Test on all platforms - Desktop, mobile, web
- Follow naming conventions - PascalCase for class names
- Export important properties - Make them editable in Inspector
- Validate inputs - Check types and ranges
- Handle edge cases - Empty strings, null values, etc.
- Keep it simple - One component, one responsibility
Common Pitfalls
❌ Don't hardcode colors
var style = StyleBoxFlat.new()
style.bg_color = Color("#1a1a2e") # BAD
✅ Use ThemeColors
var style = StyleBoxFlat.new()
style.bg_color = ThemeColors.BACKGROUND_PRIMARY # GOOD
❌ Don't create nodes in every frame
func _process(delta):
var button = Button.new() # BAD - leaks memory
add_child(button)
✅ Create once in _ready
var button: Button
func _ready():
button = Button.new()
add_child(button)
❌ Don't couple components
# In a button component
func _on_pressed():
get_parent().get_node("SomeOtherNode").do_something() # BAD
✅ Use signals
signal action_requested
func _on_pressed():
action_requested.emit() # GOOD