Agent skill

castella-core

Build desktop, web, or terminal UIs with Castella. Create widgets, components, layouts, manage reactive state, handle events, and use the theme system.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/castella-core

SKILL.md

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:

python
from castella import App, Text
from castella.frame import Frame

App(Frame("Hello", 800, 600), Text("Hello, Castella!")).run()

Install and run:

bash
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 UI
  • App(frame, widget) - Application entry point with .run()
  • Frame auto-selects platform: GLFW (desktop), Web, or Terminal
python
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:

python
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:

python
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:

python
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:

python
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:

python
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

python
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:

python
# 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:

python
Text("Hello")
    .bg_color("#1a1b26")
    .text_color("#c0caf5")
    .fixed_height(40)
    .padding(10)

Border Styling

python
# 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:

python
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

python
Button("Click me").on_click(lambda event: print("Clicked!"))

Input Changes

python
Input("initial").on_change(lambda text: print(f"New value: {text}"))

Important: Input Widget Pattern

Do NOT attach states that Input/MultilineInput manages:

python
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:

python
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

python
# 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:

python
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:

python
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:

python
Button("Submit").semantic_id("submit-btn")
Input("").semantic_id("email-input")

Best Practices

  1. Attach states: Use state.attach(self) for each observable state
  2. Fixed heights in scrollable containers: Use .fixed_height() for list items
  3. Preserve scroll: Use ScrollState to maintain scroll position
  4. Atomic list updates: Use ListState.set(items) for single rebuild
  5. Don't attach Input states: Avoid attaching states managed by Input widgets
  6. Semantic IDs: Add .semantic_id() for MCP integration

Running Scripts

bash
# 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:

bash
# 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 API
  • references/theme.md - Theme system details
  • references/animation.md - Animation patterns
  • references/state.md - State management patterns
  • scripts/ - Executable examples (counter.py, form.py, scrollable_list.py)

Didn't find tool you were looking for?

Be as detailed as possible for better results