Castella Core UI Development
Castella is a pure Python cross-platform UI framework for desktop (GLFW/SDL2), web (PyScript/Pyodide), and terminal (prompt-toolkit) applications. Write once, run everywhere with GPU-accelerated rendering via Skia.
When to use: "create a Castella app", "build a Castella UI", "Castella component", "add a button/input/text", "use reactive state", "layout with Row/Column", "change the theme", "handle click events", "preserve scroll position", "animate a widget"
Quick Start
Create a minimal Castella app:
from castella import App, Text
from castella.frame import Frame
App(Frame("Hello", 800, 600), Text("Hello, Castella!")).run()
Install and run:
uv sync --extra glfw # Desktop with GLFW
uv run python app.py
Core Concepts
App and Frame
Frame(title, width, height)- Window/container for the UIApp(frame, widget)- Application entry point with.run()- Frame auto-selects platform: GLFW (desktop), Web, or Terminal
from castella import App
from castella.frame import Frame
frame = Frame("My App", 800, 600)
app = App(frame, my_widget)
app.run()
Widgets
Base building blocks for UI elements:
| Widget | Description | Key Methods |
|--------|-------------|-------------|
| Text(content) | Display text | .font_size(n) |
| Button(label) | Clickable button | .on_click(handler) |
| Input(initial) | Single-line input | .on_change(handler) |
| MultilineInput(state) | Multi-line editor | .on_change(handler) |
| CheckBox(state) | Toggle checkbox | .on_change(handler) |
| Slider(state) | Range slider | .on_change(handler) |
| Image(path) | Local image | - |
| NetImage(url) | Remote image | - |
| Markdown(content) | Rich markdown | .on_link_click(handler) |
Layout Containers
Arrange widgets hierarchically:
from castella import Column, Row, Box
# Vertical stack
Column(
Text("Header"),
Button("Click me"),
Text("Footer"),
)
# Horizontal stack
Row(
Button("Left"),
Button("Right"),
)
# Overlapping (z-index support)
Box(
main_content,
modal_overlay.z_index(10),
)
Component Pattern
Build reactive UIs with the Component class:
from castella import Component, State, Column, Text, Button
class Counter(Component):
def __init__(self):
super().__init__()
self._count = State(0)
self._count.attach(self) # Trigger view() on change
def view(self):
return Column(
Text(f"Count: {self._count()}"),
Button("+1").on_click(lambda _: self._count.set(self._count() + 1)),
)
State Management
State[T] is an observable value that triggers UI rebuilds:
from castella import State
count = State(0) # Create with initial value
value = count() # Read current value
count.set(42) # Set new value
count += 1 # Operator support: +=, -=, *=, /=
ListState for Collections
ListState is an observable list:
from castella import ListState
items = ListState(["a", "b", "c"])
items.append("d") # Triggers rebuild
items.set(["x", "y"]) # Atomic replace (single rebuild)
Multiple States Pattern
When using multiple states, attach each to the component:
class MultiStateComponent(Component):
def __init__(self):
super().__init__()
self._tab = State("home")
self._counter = State(0)
# Attach each state
self._tab.attach(self)
self._counter.attach(self)
def view(self):
return Column(
Text(f"Tab: {self._tab()}"),
Text(f"Count: {self._counter()}"),
)
Size Policies
Control how widgets size themselves:
| Policy | Behavior |
|--------|----------|
| SizePolicy.FIXED | Exact size specified |
| SizePolicy.EXPANDING | Fill available space |
| SizePolicy.CONTENT | Size to fit content |
Fluent API Shortcuts
from castella import SizePolicy
# Fixed sizing
widget.fixed_width(100)
widget.fixed_height(40)
widget.fixed_size(200, 100)
# Content sizing
widget.fit_content() # Both dimensions
widget.fit_content_width() # Width only
widget.fit_content_height() # Height only
# Fill parent
widget.fit_parent()
Important Constraint
A Layout with CONTENT height_policy cannot have EXPANDING height children:
# This will raise RuntimeError:
Column(
Text("Hello"), # Text defaults to EXPANDING height
).height_policy(SizePolicy.CONTENT)
# Fix by setting children to FIXED or CONTENT:
Column(
Text("Hello").fixed_height(24),
).height_policy(SizePolicy.CONTENT)
Styling
Widget Styling Methods
Chain style methods on widgets:
Text("Hello")
.bg_color("#1a1b26")
.text_color("#c0caf5")
.fixed_height(40)
.padding(10)
Border Styling
# Show border with theme's default color (or custom color)
widget.show_border() # Use theme's border color
widget.show_border("#ff0000") # Use custom color
# Hide border (make it match background)
widget.erase_border()
Theme System
Access and toggle themes:
from castella.theme import ThemeManager
manager = ThemeManager()
theme = manager.current # Get current theme
manager.toggle_dark_mode() # Toggle dark/light
manager.prefer_dark(True) # Force dark mode
Built-in themes: Tokyo Night (default), Cupertino, Material Design 3
See references/theme.md for custom themes.
Event Handling
Click Events
Button("Click me").on_click(lambda event: print("Clicked!"))
Input Changes
Input("initial").on_change(lambda text: print(f"New value: {text}"))
Important: Input Widget Pattern
Do NOT attach states that Input/MultilineInput manages:
class FormComponent(Component):
def __init__(self):
super().__init__()
self._text = State("initial")
# DON'T attach - causes focus loss on every keystroke
# self._text.attach(self)
def view(self):
return Input(self._text()).on_change(lambda t: self._text.set(t))
Animation
AnimatedState
Values that animate smoothly on change:
from castella import AnimatedState
class AnimatedCounter(Component):
def __init__(self):
super().__init__()
self._value = AnimatedState(0, duration_ms=300)
self._value.attach(self)
def view(self):
return Column(
Text(f"Value: {self._value():.1f}"),
Button("+10").on_click(lambda _: self._value.set(self._value() + 10)),
)
Widget Animation Methods
# Animate to position/size
widget.animate_to(x=200, y=100, duration_ms=400)
# Slide animations
widget.slide_in("left", distance=100, duration_ms=300)
widget.slide_out("right", distance=100, duration_ms=300)
See references/animation.md for more animation patterns.
Scrollable Containers
Make layouts scrollable:
from castella import Column, ScrollState, SizePolicy
class ScrollableList(Component):
def __init__(self, items):
super().__init__()
self._items = ListState(items)
self._items.attach(self)
self._scroll = ScrollState() # Preserves scroll position
def view(self):
return Column(
*[Text(item).fixed_height(30) for item in self._items],
scrollable=True,
scroll_state=self._scroll,
).fixed_height(300)
Z-Index Stacking
Layer widgets with z-index:
from castella import Box
Box(
main_content.z_index(1),
modal_dialog.z_index(10), # Appears on top
)
Semantic IDs for MCP
Assign semantic IDs for MCP accessibility:
Button("Submit").semantic_id("submit-btn")
Input("").semantic_id("email-input")
Best Practices
- Attach states: Use
state.attach(self)for each observable state - Fixed heights in scrollable containers: Use
.fixed_height()for list items - Preserve scroll: Use
ScrollStateto maintain scroll position - Atomic list updates: Use
ListState.set(items)for single rebuild - Don't attach Input states: Avoid attaching states managed by Input widgets
- Semantic IDs: Add
.semantic_id()for MCP integration
Running Scripts
# Counter example
uv run python scripts/counter.py
# Hot reload during development
uv run python tools/hot_restarter.py scripts/counter.py
Packaging
Package your Castella app for distribution:
# Install ux bundler
uv tool install ux-py
# Create executable
ux bundle --project . --output ./dist/
See castella-packaging skill for detailed options (macOS app bundles, code signing, cross-compilation).
Reference
references/widgets.md- Complete widget APIreferences/theme.md- Theme system detailsreferences/animation.md- Animation patternsreferences/state.md- State management patternsscripts/- Executable examples (counter.py, form.py, scrollable_list.py)