Loading...
Loading...
Designs and implements custom Textual widgets with composition, styling, and lifecycle management. Use when creating reusable widget components, composing widgets from built-in components, implementing widget lifecycle (on_mount, on_unmount), handling widget state, and testing widgets. Covers custom widgets extending Static, Container, and building complex widget hierarchies.
npx skill4agent add dawiddutoit/custom-claude textual-widget-developmentfrom textual.app import ComposeResult
from textual.widgets import Static, Container
from textual.containers import Vertical
class SimpleWidget(Static):
"""A simple reusable widget."""
DEFAULT_CSS = """
SimpleWidget {
height: auto;
border: solid $primary;
padding: 1;
}
"""
def __init__(self, title: str, **kwargs: object) -> None:
super().__init__(**kwargs)
self._title = title
def render(self) -> str:
"""Render widget content."""
return f"Title: {self._title}"
# Use in app:
class MyApp(App):
def compose(self) -> ComposeResult:
yield SimpleWidget("Hello")from textual.widgets import Static, Container, Input, Button
from textual.containers import Vertical, Horizontal, Container as GenericContainer
# For custom content/display - Simple widgets
class StatusWidget(Static):
"""Displays status information."""
pass
# For layout/composition - Container widgets
class DashboardWidget(Container):
"""Composes multiple child widgets."""
pass
# Built-in widgets (ready to use)
# - Static: Display text/rich content
# - Input: Text input field
# - Button: Clickable button
# - Label: Static label
# - Select: Dropdown selector
# - DataTable: Tabular data
# - Tree: Hierarchical dataStaticContainer__init__from typing import ClassVar
from textual.app import ComposeResult
from textual.widgets import Static
class ConfigurableWidget(Static):
"""Widget with configurable parameters."""
DEFAULT_CSS = """
ConfigurableWidget {
height: auto;
border: solid $primary;
padding: 1;
}
ConfigurableWidget .header {
background: $boost;
text-style: bold;
}
"""
# Class constants
BORDER_COLOR: ClassVar[str] = "$primary"
def __init__(
self,
title: str,
content: str = "",
*,
name: str | None = None,
id: str | None = None, # noqa: A002
classes: str | None = None,
variant: str = "default",
) -> None:
"""Initialize widget.
Args:
title: Widget title.
content: Initial content.
name: Widget name.
id: Widget ID for querying.
classes: CSS classes to apply.
variant: Visual variant (default, compact, etc).
Always pass **kwargs to parent:
super().__init__(name=name, id=id, classes=classes)
"""
super().__init__(name=name, id=id, classes=classes)
self._title = title
self._content = content
self._variant = variantsuper().__init__()_*from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Static, Button, Label
class CompositeWidget(Vertical):
"""Widget that composes multiple child widgets."""
DEFAULT_CSS = """
CompositeWidget {
height: auto;
border: solid $primary;
}
CompositeWidget .header {
height: 3;
background: $boost;
text-style: bold;
}
CompositeWidget .content {
height: 1fr;
overflow: auto;
}
CompositeWidget .footer {
height: auto;
border-top: solid $primary;
}
"""
def __init__(self, title: str, **kwargs: object) -> None:
super().__init__(**kwargs)
self._title = title
self._items: list[str] = []
def compose(self) -> ComposeResult:
"""Compose child widgets.
Yields:
Child widgets in order they should appear.
"""
# Header
yield Static(f"Title: {self._title}", classes="header")
# Content area with items
yield Vertical(
Static(
"Content area" if not self._items else "\n".join(self._items),
id="content-area",
),
classes="content",
)
# Footer with buttons
yield Horizontal(
Button("Add", id="btn-add", variant="primary"),
Button("Remove", id="btn-remove"),
classes="footer",
)
async def on_mount(self) -> None:
"""Initialize after composition."""
# Can now query child widgets
content = self.query_one("#content-area", Static)
content.update("Initialized")
async def add_item(self, item: str) -> None:
"""Add item to widget."""
self._items.append(item)
content = self.query_one("#content-area", Static)
content.update("\n".join(self._items))compose()on_mount()self.query_one()render()from rich.console import Console
from rich.table import Table
from rich.text import Text
from textual.widgets import Static
class RichWidget(Static):
"""Widget that renders Rich objects."""
def __init__(self, data: dict, **kwargs: object) -> None:
super().__init__(**kwargs)
self._data = data
def render(self) -> str | Text | Table:
"""Render widget content as Rich object.
Returns:
str, Text, or Rich-renderable object.
Textual converts to displayable content.
"""
# Simple text
return f"Data: {self._data}"
# Rich Text with styling
text = Text()
text.append("Status: ", style="bold")
text.append(self._data.get("status", "unknown"), style="green")
return text
# Rich Table
table = Table(title="Data")
table.add_column("Key", style="cyan")
table.add_column("Value", style="magenta")
for key, value in self._data.items():
table.add_row(key, str(value))
return tablerender()update(content)refresh()class LifecycleWidget(Static):
"""Widget with full lifecycle implementation."""
def __init__(self, **kwargs: object) -> None:
super().__init__(**kwargs)
self._initialized = False
async def on_mount(self) -> None:
"""Called when widget is mounted to DOM.
Use for:
- Initializing state
- Starting background tasks
- Loading data
- Querying sibling widgets
"""
self._initialized = True
self.update("Widget mounted and ready")
# Start background task
self.app.run_worker(self._background_work())
async def _background_work(self) -> None:
"""Background async work."""
import asyncio
while self._initialized:
await asyncio.sleep(1)
self.refresh()
def on_unmount(self) -> None:
"""Called when widget is removed from DOM.
Use for:
- Cleanup
- Stopping background tasks
- Closing connections
"""
self._initialized = Falseon_mount()on_unmount()on_focus()on_blur()from textual.message import Message
from textual import on
from textual.widgets import Button
class ItemWidget(Static):
"""Widget that posts custom messages."""
class ItemClicked(Message):
"""Posted when item is clicked."""
def __init__(self, item_id: str) -> None:
super().__init__()
self.item_id = item_id
def __init__(self, item_id: str, label: str, **kwargs: object) -> None:
super().__init__(**kwargs)
self._item_id = item_id
self._label = label
def render(self) -> str:
return f"[{self._item_id}] {self._label}"
def on_click(self) -> None:
"""Handle click event."""
# Post message that parent can handle
self.post_message(self.ItemClicked(self._item_id))
# Parent widget handling custom messages
class ItemList(Vertical):
"""Widget that handles ItemWidget messages."""
@on(ItemWidget.ItemClicked)
async def on_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
"""Handle item click message."""
print(f"Item clicked: {message.item_id}")
self.notify(f"Selected: {message.item_id}")self.post_message()@on(MessageType)class StyledWidget(Static):
"""Widget with comprehensive CSS styling."""
DEFAULT_CSS = """
StyledWidget {
width: 100%;
height: auto;
border: solid $primary;
padding: 1 2;
background: $surface;
}
StyledWidget .header {
width: 100%;
height: 3;
background: $boost;
text-style: bold;
content-align: center middle;
color: $text;
}
StyledWidget .content {
width: 1fr;
height: 1fr;
padding: 1;
overflow: auto;
}
StyledWidget .status-active {
color: $success;
text-style: bold;
}
StyledWidget .status-inactive {
color: $error;
text-style: dim;
}
StyledWidget:focus {
border: double $primary;
}
StyledWidget:disabled {
opacity: 0.5;
}
"""
def render(self) -> str:
return "Styled Widget"def create_section(title: str, content: Static) -> Container:
"""Factory function for creating sections."""
return Container(
Static(title, classes="section-header"),
content,
classes="section",
)async def lazy_mount_children(self) -> None:
"""Mount child widgets gradually."""
for i, item in enumerate(self._items):
await container.mount(self._create_item_widget(item))
if i % 10 == 0:
await asyncio.sleep(0) # Yield to event loop