Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:08:16 +08:00
commit fc569e5620
38 changed files with 4997 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
---
name: textual-builder
description: Build Text User Interface (TUI) applications using the Textual Python framework (v0.86.0+). Use when creating terminal-based applications, prototyping card games or interactive CLIs, or when the user mentions Textual, TUI, or terminal UI. Includes comprehensive reference documentation, card game starter template, and styling guides.
---
# Textual Builder
## Overview
This skill helps you build sophisticated Text User Interfaces (TUIs) using Textual, a Python framework for creating terminal and browser-based applications with a modern web-inspired API. It includes reference documentation, a card game template, and best practices for Textual development.
## Quick Start
### Basic Textual App
```python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Label
class MyApp(App):
def compose(self) -> ComposeResult:
yield Header()
yield Label("Hello, Textual!")
yield Footer()
if __name__ == "__main__":
app = MyApp()
app.run()
```
### Card Game Template
For card game prototyping, copy the template:
```bash
cp -r assets/card-game-template/* ./my-game/
cd my-game
python app.py
```
The template includes:
- Interactive Card widget with face-up/down states
- Hand containers for player cards
- Play area with turn management
- Key bindings for card selection and playing
- Customizable styling
See `assets/card-game-template/README.md` for customization guide.
## When to Read Reference Documentation
This skill includes comprehensive reference files. Load them based on your task:
### references/basics.md
**Read when:** Setting up app structure, using reactive attributes, handling mounting, querying widgets, or working with messages/events.
**Covers:**
- App structure and compose method
- Reactive attributes and watchers
- Mounting and dynamic widget creation
- Widget querying
- Messages, events, and custom messages
### references/widgets.md
**Read when:** Adding UI elements like buttons, inputs, labels, data tables, or creating custom widgets.
**Covers:**
- Display widgets (Label, Static, Placeholder)
- Input widgets (Button, Input, TextArea, Switch)
- DataTable for tabular data
- Layout containers (Container, Grid, Horizontal, Vertical)
- Custom widget creation
- Header/Footer
### references/layout.md
**Read when:** Designing layouts, positioning widgets, using grid systems, or handling responsive sizing.
**Covers:**
- Layout types (vertical, horizontal, grid)
- Grid configuration (cell spanning, row/column sizing)
- Alignment and content positioning
- Docking widgets to screen edges
- Sizing (fixed, relative, fractional, auto)
- Spacing (margin, padding)
- Scrolling
### references/styling.md
**Read when:** Applying CSS styles, theming, adding borders, or customizing widget appearance.
**Covers:**
- CSS files and selectors
- Colors (named, hex, RGB, theme variables)
- Borders and border styling
- Text styling and alignment
- Opacity and tinting
- Rich markup for styled text
- Pseudo-classes (:hover, :focus, etc.)
### references/interactivity.md
**Read when:** Implementing keyboard shortcuts, handling mouse events, responding to user actions, or creating interactive behaviors.
**Covers:**
- Key bindings and actions
- Dynamic binding updates
- Mouse events (click, hover, enter, leave)
- Keyboard events
- Focus management
- Widget-specific messages
- Custom messages
- Notifications and timers
## Common Workflows
### Creating a New TUI App
1. Start with basic app structure (see Quick Start)
2. Design layout (read `references/layout.md`)
3. Add widgets (read `references/widgets.md`)
4. Style with CSS (read `references/styling.md`)
5. Add interactivity (read `references/interactivity.md`)
### Prototyping a Card Game
1. Copy the card game template
2. Customize the Card widget for your game's card properties
3. Modify game logic in action methods
4. Add game-specific rules in message handlers
5. Style cards and layout in `app.tcss`
### Adding Interactive Features
1. Define key bindings in `BINDINGS`
2. Implement action methods (`action_*`)
3. Handle widget messages (`on_button_pressed`, etc.)
4. Use reactive attributes for state management
5. Update UI in watchers
## Best Practices
- **Progressive Development**: Start simple, add complexity incrementally
- **Reactive State**: Use `reactive()` for state that affects UI
- **CSS Separation**: Keep styling in `.tcss` files, not inline
- **Widget Reuse**: Create custom widgets for repeated components
- **Message Bubbling**: Use `event.stop()` to control message propagation
- **Type Hints**: Use proper type hints for better IDE support
- **IDs and Classes**: Use semantic IDs/classes for querying and styling
## Installation
```bash
pip install textual
# or
uv pip install textual
```
Current version: v0.86.0+ (as of November 2025, latest is v6.6.0)
## Resources
### references/
Comprehensive documentation loaded on-demand:
- `basics.md` - Core concepts and app structure
- `widgets.md` - Widget catalog and usage
- `layout.md` - Layout systems and positioning
- `styling.md` - CSS and theming
- `interactivity.md` - Events, bindings, and actions
### assets/
- `card-game-template/` - Complete starter template for card games with interactive cards, hands, and turn management
## Official Documentation
For topics not covered in this skill, consult:
- https://textual.textualize.io/ (official docs)
- https://github.com/Textualize/textual (GitHub repo)

View File

@@ -0,0 +1,77 @@
# Card Game Template
A starter template for building turn-based card games with Textual.
## Features
- **Card Widget**: Customizable playing cards with suit, rank, face-up/down state
- **Hand Container**: Display and manage player hands
- **Play Area**: Central area for cards in play
- **Turn System**: Basic turn management
- **Interactivity**: Card selection and playing with keyboard shortcuts
## Running the Template
```bash
python app.py
```
## Key Bindings
- `d` - Draw a card
- `space` - Select/deselect card
- `p` - Play selected card
- `n` - Next turn
- `q` - Quit
## Customization
### Card Values
Modify the `Card` class to add game-specific properties:
```python
class Card(Widget):
def __init__(self, rank: str, suit: str, power: int = 0, special_ability: str = ""):
self.power = power
self.special_ability = special_ability
# ...
```
### Game Rules
Implement game logic in the `CardGameApp` methods:
- `on_card_played()` - Validate and process card plays
- `action_draw_card()` - Implement deck management
- `action_next_turn()` - Add turn-based game logic
### Card Appearance
Edit `Card.compose()` or the CSS in `app.tcss` to change card styling.
### Deck Management
Add a `Deck` class to manage card shuffling and drawing:
```python
class Deck:
def __init__(self):
self.cards = []
self.shuffle()
def shuffle(self):
import random
random.shuffle(self.cards)
def draw(self) -> Card | None:
return self.cards.pop() if self.cards else None
```
## Structure
```
card-game-template/
├── app.py # Main application
├── app.tcss # Styles
└── README.md # This file
```

View File

@@ -0,0 +1,324 @@
"""
Card Game Template for Textual
A starter template for building turn-based card games with Textual.
Includes:
- Card widget with customizable suit, rank, and face-up/down state
- Hand container for displaying player hands
- Play area for cards in play
- Turn management system
- Action system with key bindings
Customize this template for your specific card game rules.
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, Label, Static
from textual.widget import Widget
from textual.reactive import reactive
from textual.message import Message
from textual.binding import Binding
class Card(Widget):
"""A playing card widget."""
DEFAULT_CSS = """
Card {
width: 12;
height: 10;
border: round white;
background: $panel;
content-align: center middle;
}
Card:hover {
background: $boost;
border: heavy $primary;
}
Card.selected {
border: double cyan;
background: $accent;
}
Card.face-down {
background: $surface;
color: $text-muted;
}
Card.disabled {
opacity: 0.5;
}
.card-rank {
text-style: bold;
text-align: center;
}
.card-suit {
text-align: center;
}
"""
class Selected(Message):
"""Posted when card is selected."""
def __init__(self, card: "Card") -> None:
super().__init__()
self.card = card
class Played(Message):
"""Posted when card is played."""
def __init__(self, card: "Card") -> None:
super().__init__()
self.card = card
face_up = reactive(True)
selectable = reactive(True)
def __init__(
self,
rank: str,
suit: str,
value: int = 0,
face_up: bool = True,
card_id: str | None = None,
) -> None:
super().__init__(id=card_id)
self.rank = rank
self.suit = suit
self.value = value
self.face_up = face_up
def compose(self) -> ComposeResult:
if self.face_up:
yield Label(self.rank, classes="card-rank")
yield Label(self.suit, classes="card-suit")
else:
yield Label("🂠", classes="card-back")
def watch_face_up(self, face_up: bool) -> None:
"""Update display when card is flipped."""
if face_up:
self.remove_class("face-down")
else:
self.add_class("face-down")
# Refresh the card content
self.recompose()
def on_click(self) -> None:
"""Handle card click."""
if self.selectable:
self.post_message(self.Selected(self))
def flip(self) -> None:
"""Flip the card."""
self.face_up = not self.face_up
def play(self) -> None:
"""Play this card."""
if self.selectable:
self.post_message(self.Played(self))
class Hand(Container):
"""Container for a player's hand of cards."""
DEFAULT_CSS = """
Hand {
layout: horizontal;
height: auto;
width: 100%;
align: center middle;
}
Hand > Card {
margin: 0 1;
}
"""
def __init__(self, player_name: str, **kwargs) -> None:
super().__init__(**kwargs)
self.player_name = player_name
def add_card(self, card: Card) -> None:
"""Add a card to this hand."""
self.mount(card)
def remove_card(self, card: Card) -> None:
"""Remove a card from this hand."""
card.remove()
def get_cards(self) -> list[Card]:
"""Get all cards in this hand."""
return list(self.query(Card))
class PlayArea(Container):
"""Central area where cards are played."""
DEFAULT_CSS = """
PlayArea {
height: 1fr;
border: round $primary;
background: $surface;
align: center middle;
}
PlayArea > .play-area-label {
color: $text-muted;
}
"""
def compose(self) -> ComposeResult:
yield Label("Play Area", classes="play-area-label")
class GameState(Static):
"""Display current game state."""
DEFAULT_CSS = """
GameState {
dock: top;
height: 3;
background: $boost;
content-align: center middle;
text-style: bold;
}
"""
current_player = reactive("Player 1")
turn = reactive(1)
def render(self) -> str:
return f"Turn {self.turn} | Current Player: {self.current_player}"
class CardGameApp(App):
"""A card game application."""
CSS_PATH = "app.tcss"
BINDINGS = [
("q", "quit", "Quit"),
("n", "next_turn", "Next Turn"),
("d", "draw_card", "Draw Card"),
("p", "play_selected", "Play Card"),
Binding("space", "toggle_select", "Select/Deselect", show=True),
]
selected_card: Card | None = None
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield GameState(id="game-state")
with Vertical(id="game-container"):
# Opponent's hand (face down)
with Container(id="opponent-area"):
yield Label("Opponent", id="opponent-label")
yield Hand("Opponent", id="opponent-hand")
# Play area
yield PlayArea(id="play-area")
# Player's hand (face up)
with Container(id="player-area"):
yield Hand("Player", id="player-hand")
yield Label("Your Hand", id="player-label")
yield Footer()
def on_mount(self) -> None:
"""Initialize the game when app starts."""
# Deal initial cards (example)
player_hand = self.query_one("#player-hand", Hand)
opponent_hand = self.query_one("#opponent-hand", Hand)
# Example: Deal 5 cards to each player
suits = ["", "", "", ""]
ranks = ["A", "2", "3", "4", "5"]
for i, rank in enumerate(ranks):
# Player cards (face up)
player_hand.add_card(Card(rank, suits[i % 4], face_up=True, card_id=f"player-{i}"))
# Opponent cards (face down)
opponent_hand.add_card(Card(rank, suits[i % 4], face_up=False, card_id=f"opp-{i}"))
def on_card_selected(self, event: Card.Selected) -> None:
"""Handle card selection."""
# Deselect previous
if self.selected_card:
self.selected_card.remove_class("selected")
# Select new card
self.selected_card = event.card
event.card.add_class("selected")
self.notify(f"Selected {event.card.rank} of {event.card.suit}")
def on_card_played(self, event: Card.Played) -> None:
"""Handle card being played."""
play_area = self.query_one("#play-area", PlayArea)
card = event.card
# Remove from hand
hand = card.parent
if isinstance(hand, Hand):
hand.remove_card(card)
# Move to play area
play_area.mount(card)
self.notify(f"Played {card.rank} of {card.suit}")
# Deselect
if self.selected_card == card:
self.selected_card = None
def action_next_turn(self) -> None:
"""Advance to next turn."""
game_state = self.query_one(GameState)
game_state.turn += 1
# Toggle current player
if game_state.current_player == "Player 1":
game_state.current_player = "Player 2"
else:
game_state.current_player = "Player 1"
self.notify(f"Turn {game_state.turn}")
def action_draw_card(self) -> None:
"""Draw a card (example)."""
player_hand = self.query_one("#player-hand", Hand)
# Example: Draw a random card
import random
suits = ["", "", "", ""]
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
card = Card(random.choice(ranks), random.choice(suits), face_up=True)
player_hand.add_card(card)
self.notify("Drew a card")
def action_play_selected(self) -> None:
"""Play the currently selected card."""
if self.selected_card:
self.selected_card.play()
else:
self.notify("No card selected", severity="warning")
def action_toggle_select(self) -> None:
"""Select/deselect hovered card."""
# This is a simplified version - in practice you'd track the hovered card
if self.selected_card:
self.selected_card.remove_class("selected")
self.selected_card = None
if __name__ == "__main__":
app = CardGameApp()
app.run()

View File

@@ -0,0 +1,51 @@
/* Card Game Template Styles */
Screen {
background: $background;
}
#game-container {
height: 100%;
width: 100%;
layout: vertical;
}
/* Opponent Area */
#opponent-area {
height: auto;
padding: 1 2;
background: $surface;
}
#opponent-label {
text-align: center;
text-style: bold;
color: $text-muted;
}
#opponent-hand {
padding: 1 0;
}
/* Play Area */
#play-area {
height: 1fr;
margin: 1 2;
}
/* Player Area */
#player-area {
height: auto;
padding: 1 2;
background: $boost;
}
#player-label {
text-align: center;
text-style: bold;
color: $text;
}
#player-hand {
padding: 1 0;
}

View File

@@ -0,0 +1,182 @@
# Textual Basics
## App Structure
Every Textual app follows this pattern:
```python
from textual.app import App, ComposeResult
from textual.widgets import Widget
class MyApp(App):
"""Docstring describing the app."""
# Optional: Link to external CSS file
CSS_PATH = "app.tcss"
# Optional: Inline CSS
CSS = """
Screen {
align: center middle;
}
"""
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Widget()
def on_mount(self) -> None:
"""Called when app is mounted and ready."""
pass
if __name__ == "__main__":
app = MyApp()
app.run()
```
## Compose Method
The `compose()` method yields widgets to add to the app. It's called once during initialization:
```python
def compose(self) -> ComposeResult:
yield Header()
yield ContentWidget()
yield Footer()
```
## Mounting
- `on_mount()`: Called when the app/widget is fully mounted and ready
- `mount()`: Dynamically add widgets after app starts (returns a coroutine)
```python
async def on_key(self) -> None:
# Must await when modifying mounted widgets
await self.mount(NewWidget())
self.query_one(Button).label = "Modified!"
```
## Reactive Attributes
Reactive attributes automatically update the UI when changed:
```python
from textual.reactive import reactive
class Counter(Widget):
count = reactive(0) # Initial value
def watch_count(self, new_value: int) -> None:
"""Called automatically when count changes."""
self.query_one(Label).update(f"Count: {new_value}")
def increment(self) -> None:
self.count += 1 # Triggers watch_count
```
### Reactive with Bindings
Set `bindings=True` to auto-refresh footer bindings when reactive changes:
```python
class MyApp(App):
page = reactive(0, bindings=True)
def check_action(self, action: str, parameters) -> bool | None:
"""Return None to disable action."""
if action == "next" and self.page == MAX_PAGES:
return None # Dims the key in footer
return True
```
## Querying Widgets
Find widgets in the DOM:
```python
# Get one widget (raises if not found)
button = self.query_one(Button)
button = self.query_one("#my-id")
# Get multiple widgets
all_buttons = self.query(Button)
for button in all_buttons:
pass
# Get with CSS selector
widget = self.query_one("#container .special-class")
```
## Messages and Events
### Built-in Events
Handle with `on_<event>` methods:
```python
def on_mount(self) -> None:
"""When mounted."""
pass
def on_key(self, event: events.Key) -> None:
"""Key pressed."""
if event.key == "escape":
self.exit()
```
### Widget Messages
Handle messages from child widgets:
```python
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Button was clicked."""
self.notify(f"Button {event.button.id} clicked!")
def on_input_changed(self, event: Input.Changed) -> None:
"""Input text changed."""
self.value = event.value
```
### Custom Messages
Define custom messages in your widgets:
```python
from textual.message import Message
class MyWidget(Widget):
class ValueChanged(Message):
"""Posted when value changes."""
def __init__(self, value: int) -> None:
super().__init__()
self.value = value
def update_value(self, new_value: int) -> None:
self.value = new_value
self.post_message(self.ValueChanged(new_value))
# Handle in parent
def on_my_widget_value_changed(self, event: MyWidget.ValueChanged) -> None:
self.notify(f"New value: {event.value}")
```
## Preventing Message Propagation
Stop messages from bubbling to parent:
```python
def on_switch_changed(self, event: Switch.Changed) -> None:
event.stop() # Don't propagate to parent
# Handle here
```
## Preventing Reactive Watchers
Temporarily prevent reactive watchers from firing:
```python
with self.prevent(MyWidget.ValueChanged):
self.value = new_value # Won't trigger watch_value or post message
```

View File

@@ -0,0 +1,346 @@
# Interactivity: Events, Bindings, and Actions
## Key Bindings
Define keyboard shortcuts:
```python
from textual.app import App
from textual.binding import Binding
class MyApp(App):
BINDINGS = [
("q", "quit", "Quit"), # key, action, description
("s", "save", "Save"),
("ctrl+c", "copy", "Copy"),
Binding("f1", "help", "Help", show=True, priority=True),
]
def action_save(self) -> None:
"""Actions are methods prefixed with 'action_'."""
self.notify("Saved!")
def action_copy(self) -> None:
self.notify("Copied!")
def action_help(self) -> None:
self.notify("Help content...")
```
### Binding Options
```python
Binding(
key="f1",
action="help",
description="Help",
show=True, # Show in footer (default: True)
priority=True, # Prioritize over widget bindings
)
```
### Dynamic Bindings
Refresh bindings when state changes:
```python
class MyApp(App):
page = reactive(0, bindings=True) # Auto-refresh bindings
def check_action(self, action: str, parameters) -> bool | None:
"""Control action availability."""
if action == "next" and self.page >= MAX_PAGES:
return None # Disables and dims the key
if action == "previous" and self.page == 0:
return None
return True # Enabled
```
Or manually refresh:
```python
def update_state(self):
self.state = "new_state"
self.refresh_bindings() # Update footer
```
## Mouse Events
Handle mouse interactions:
```python
from textual import events
class MyWidget(Widget):
def on_click(self, event: events.Click) -> None:
"""Widget was clicked."""
self.notify(f"Clicked at {event.x}, {event.y}")
def on_mouse_move(self, event: events.MouseMove) -> None:
"""Mouse moved over widget."""
pass
def on_enter(self, event: events.Enter) -> None:
"""Mouse entered widget."""
self.add_class("hover")
def on_leave(self, event: events.Leave) -> None:
"""Mouse left widget."""
self.remove_class("hover")
```
## Keyboard Events
Handle key presses:
```python
from textual import events
class MyApp(App):
def on_key(self, event: events.Key) -> None:
"""Any key pressed."""
if event.key == "escape":
self.exit()
elif event.key == "space":
self.toggle_pause()
def key_r(self, event: events.Key) -> None:
"""Specific key handler (press 'r')."""
self.reset()
```
## Focus Events
Track focus changes:
```python
def on_focus(self, event: events.Focus) -> None:
"""Widget gained focus."""
self.border_title = "Focused"
def on_blur(self, event: events.Blur) -> None:
"""Widget lost focus."""
self.border_title = ""
```
Programmatic focus:
```python
widget.focus() # Give focus to widget
widget.can_focus = True # Enable focusing (default for inputs)
```
## Widget Messages
Handle messages from specific widgets:
```python
from textual.widgets import Button, Input, Switch
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Any button pressed."""
button_id = event.button.id
self.notify(f"Button {button_id} pressed")
def on_input_changed(self, event: Input.Changed) -> None:
"""Input text changed."""
self.update_preview(event.value)
def on_input_submitted(self, event: Input.Submitted) -> None:
"""User pressed Enter in input."""
self.process(event.value)
def on_switch_changed(self, event: Switch.Changed) -> None:
"""Switch toggled."""
self.feature_enabled = event.value
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
"""Row in table selected."""
row_key = event.row_key
```
### Message Naming Convention
Handler method: `on_{widget_type}_{message_name}`
- Converts to snake_case
- Example: `Button.Pressed``on_button_pressed`
- Custom widget: `MyWidget.ValueChanged``on_my_widget_value_changed`
## Custom Messages
Define custom messages for your widgets:
```python
from textual.message import Message
from textual.widget import Widget
class Card(Widget):
class Selected(Message):
"""Posted when card is selected."""
def __init__(self, card_id: str, value: int) -> None:
super().__init__()
self.card_id = card_id
self.value = value
def on_click(self) -> None:
self.post_message(self.Selected(self.id, self.value))
# Handle in parent
def on_card_selected(self, event: Card.Selected) -> None:
self.notify(f"Card {event.card_id} (value: {event.value}) selected")
```
## Message Control
### Stop Propagation
Prevent message from bubbling to parent:
```python
def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop() # Don't propagate to parent
# Handle locally
```
### Prevent Messages
Temporarily suppress messages:
```python
with widget.prevent(Switch.Changed):
widget.value = True # Won't emit Changed message
```
Useful when programmatically updating to avoid infinite loops.
## Actions
Actions are methods that can be triggered by bindings or programmatically:
```python
class MyApp(App):
BINDINGS = [
("n", "next_page", "Next"),
("p", "prev_page", "Previous"),
]
def action_next_page(self) -> None:
self.page += 1
self.refresh_view()
def action_prev_page(self) -> None:
self.page -= 1
self.refresh_view()
```
### Parameterized Actions
Pass parameters to actions:
```python
BINDINGS = [
("r", "add_color('red')", "Red"),
("g", "add_color('green')", "Green"),
("b", "add_color('blue')", "Blue"),
]
def action_add_color(self, color: str) -> None:
self.add_widget(ColorBar(color))
```
### Programmatic Action Calls
```python
self.run_action("save") # Trigger action by name
```
## Notifications
Show temporary messages to user:
```python
self.notify("File saved successfully!")
self.notify("Error occurred", severity="error")
self.notify("Warning!", severity="warning")
self.notify("Info message", severity="information", timeout=5)
```
## Timers
Schedule repeated actions:
```python
def on_mount(self) -> None:
self.set_interval(1.0, self.update_timer) # Every 1 second
def update_timer(self) -> None:
self.elapsed += 1
self.query_one("#timer").update(str(self.elapsed))
```
One-time delayed action:
```python
self.set_timer(2.0, self.delayed_action) # After 2 seconds
def delayed_action(self) -> None:
self.notify("Timer complete!")
```
## Example: Interactive Card Selection
```python
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.widget import Widget
from textual.widgets import Label, Static
from textual.message import Message
class Card(Widget):
DEFAULT_CSS = """
Card {
width: 12;
height: 10;
border: round white;
background: $panel;
}
Card:hover {
background: $boost;
}
Card.selected {
border: double cyan;
background: $accent;
}
"""
class Selected(Message):
def __init__(self, card: "Card") -> None:
super().__init__()
self.card = card
def __init__(self, suit: str, value: str) -> None:
super().__init__()
self.suit = suit
self.value = value
def compose(self) -> ComposeResult:
yield Label(f"{self.value}\n{self.suit}")
def on_click(self) -> None:
self.post_message(self.Selected(self))
class CardGame(App):
def compose(self) -> ComposeResult:
with Horizontal(id="hand"):
yield Card("", "A")
yield Card("", "K")
yield Card("", "Q")
def on_card_selected(self, event: Card.Selected) -> None:
# Deselect all
for card in self.query(Card):
card.remove_class("selected")
# Select clicked
event.card.add_class("selected")
self.notify(f"Selected {event.card.value} of {event.card.suit}")
```

View File

@@ -0,0 +1,298 @@
# Layout and Positioning
## Layout Types
### Vertical (Default)
Stacks widgets vertically:
```css
Container {
layout: vertical;
}
```
### Horizontal
Arranges widgets side-by-side:
```css
Container {
layout: horizontal;
}
```
### Grid
Grid layout with rows and columns:
```css
Grid {
layout: grid;
grid-size: 3 2; /* 3 columns, 2 rows */
grid-gutter: 1 2; /* vertical horizontal spacing */
}
```
#### Grid Cell Spanning
Make widgets span multiple cells:
```css
#header {
column-span: 3; /* Span 3 columns */
}
#sidebar {
row-span: 2; /* Span 2 rows */
}
```
#### Grid Rows and Columns
Define row heights and column widths:
```css
Grid {
grid-size: 2 3;
grid-rows: 1fr 6 25%; /* Flexible, fixed 6, 25% */
grid-columns: 1fr 2fr; /* 1:2 ratio */
}
```
## Alignment
### Screen/Container Alignment
Center content within screen:
```css
Screen {
align: center middle; /* horizontal vertical */
}
```
Options: `left`, `center`, `right` × `top`, `middle`, `bottom`
### Content Alignment
Align content within a widget:
```css
MyWidget {
content-align: center middle;
text-align: center;
}
```
## Docking
Pin widgets to screen edges:
```css
#header {
dock: top;
height: 3;
}
#sidebar {
dock: left;
width: 20;
}
#footer {
dock: bottom;
}
```
Docking order matters - earlier docked widgets take priority.
## Sizing
### Fixed Sizes
```css
Widget {
width: 50; /* 50 cells */
height: 10; /* 10 rows */
}
```
### Relative Sizes
```css
Widget {
width: 50%; /* 50% of parent */
height: 100%;
}
```
### Fractional Units
Share available space proportionally:
```css
#left {
width: 1fr; /* Gets 1 part */
}
#right {
width: 2fr; /* Gets 2 parts (twice as wide) */
}
```
### Auto Sizing
Fit content:
```css
Widget {
width: auto;
height: auto;
}
```
### Min/Max Constraints
```css
Widget {
min-width: 20;
max-width: 80;
min-height: 5;
max-height: 30;
}
```
## Spacing
### Margin
Space outside widget border:
```css
Widget {
margin: 1; /* All sides */
margin: 1 2; /* vertical horizontal */
margin: 1 2 3 4; /* top right bottom left */
}
```
### Padding
Space inside widget border:
```css
Widget {
padding: 1; /* All sides */
padding: 1 2; /* vertical horizontal */
}
```
## Visibility
### Display
Show or hide widgets:
```css
#hidden {
display: none;
}
#visible {
display: block;
}
```
Toggle in Python:
```python
widget.display = False # Hide
widget.display = True # Show
```
### Visibility
Similar to display but reserves space:
```css
Widget {
visibility: hidden; /* Hidden but takes space */
visibility: visible;
}
```
## Layers
Control stacking order:
```css
#background {
layer: below;
}
#popup {
layer: above;
}
```
## Scrolling
### Enable Scrolling
```css
Container {
overflow-x: auto; /* Horizontal scrolling */
overflow-y: auto; /* Vertical scrolling */
overflow: auto auto; /* Both */
}
```
### Programmatic Scrolling
```python
# Scroll to specific position
container.scroll_to(x=0, y=100)
# Scroll widget into view
widget.scroll_visible()
# Scroll to end
self.screen.scroll_end(animate=True)
```
## Example: Card Game Layout
```css
Screen {
layout: vertical;
}
#opponent-hand {
dock: top;
height: 12;
layout: horizontal;
align: center top;
}
#play-area {
height: 1fr;
layout: grid;
grid-size: 5 3;
align: center middle;
}
#player-hand {
dock: bottom;
height: 15;
layout: horizontal;
align: center bottom;
padding: 1;
}
.card {
width: 12;
height: 10;
margin: 0 1;
}
```

View File

@@ -0,0 +1,323 @@
# Styling with CSS
## CSS Files
Link external CSS file:
```python
class MyApp(App):
CSS_PATH = "app.tcss" # Textual CSS file
```
Or inline CSS:
```python
class MyApp(App):
CSS = """
Screen {
background: $background;
}
"""
```
## Selectors
### Type Selectors
Target all widgets of a type:
```css
Button {
width: 100%;
}
Label {
color: cyan;
}
```
### ID Selectors
Target specific widget:
```css
#my-button {
background: red;
}
#header {
dock: top;
}
```
### Class Selectors
Target widgets with specific class:
```css
.card {
border: round white;
padding: 1;
}
.selected {
background: yellow;
}
```
Add classes in Python:
```python
widget = Label("Text", classes="card selected")
# or
widget.add_class("highlighted")
widget.remove_class("selected")
widget.toggle_class("active")
```
### Pseudo-classes
Style based on state:
```css
Button:hover {
background: $accent;
}
Button:focus {
border: double green;
}
Input:disabled {
opacity: 0.5;
}
```
Common pseudo-classes: `:hover`, `:focus`, `:focus-within`, `:disabled`, `:enabled`
### Combinators
```css
/* Direct children */
Container > Label {
color: white;
}
/* Descendants */
Container Label {
margin: 1;
}
/* Class and type */
Label.card {
border: round;
}
```
## Colors
### Named Colors
```css
Widget {
color: red;
background: blue;
border: green;
}
```
### Hex Colors
```css
Widget {
color: #ff0000;
background: #00ff0088; /* With alpha */
}
```
### RGB/RGBA
```css
Widget {
color: rgb(255, 0, 0);
background: rgba(0, 255, 0, 0.5);
}
```
### Theme Variables
Use built-in theme colors:
```css
Widget {
background: $background;
color: $text;
border: $primary;
}
```
Common theme variables:
- `$background` - Main background
- `$surface` - Surface color
- `$panel` - Panel background
- `$boost` - Highlighted background
- `$primary` - Primary accent
- `$secondary` - Secondary accent
- `$accent` - Accent color
- `$text` - Main text color
- `$text-muted` - Muted text
- `$foreground-muted` - Dimmed foreground
## Borders
### Border Styles
```css
Widget {
border: solid red; /* Style and color */
border: round cyan; /* Rounded border */
border: double white; /* Double line */
border: dashed yellow; /* Dashed */
border: heavy green; /* Heavy/thick */
border: tall blue; /* Tall characters */
}
```
### Border Sides
```css
Widget {
border-top: solid red;
border-bottom: round blue;
border-left: double green;
border-right: dashed yellow;
}
```
### Border Title
```css
Widget {
border: round white;
border-title-align: center;
}
```
Set title in Python:
```python
widget.border_title = "My Widget"
```
## Text Styling
### Text Properties
```css
Label {
text-style: bold;
text-style: italic;
text-style: bold italic;
text-style: underline;
text-style: strike;
}
```
### Text Alignment
```css
Static {
text-align: left;
text-align: center;
text-align: right;
}
```
## Keylines
Add separators between grid cells or flex items:
```css
Grid {
keyline: thin green;
keyline: thick $primary;
}
```
Note: Must be on a container with a layout.
## Opacity
```css
Widget {
opacity: 0.5; /* 50% transparent */
opacity: 0; /* Fully transparent */
opacity: 1; /* Fully opaque */
}
```
## Tint
Apply color overlay:
```css
Widget {
tint: rgba(255, 0, 0, 0.3); /* Red tint */
}
```
## Rich Markup
Use Rich markup in text:
```python
label = Label("[bold cyan]Hello[/bold cyan] [red]World[/red]")
label.update("[underline]Updated[/underline]")
```
Common markup:
- `[bold]...[/bold]` - Bold
- `[italic]...[/italic]` - Italic
- `[color]...[/color]` - Colored (e.g., `[red]`, `[#ff0000]`)
- `[underline]...[/underline]` - Underline
- `[strike]...[/strike]` - Strikethrough
- `[link=...]...[/link]` - Link
## Example: Card Styling
```css
.card {
width: 12;
height: 10;
border: round $secondary;
background: $panel;
padding: 1;
content-align: center middle;
}
.card:hover {
background: $boost;
border: heavy $primary;
}
.card.selected {
background: $accent;
border: double $primary;
}
.card.disabled {
opacity: 0.5;
tint: rgba(0, 0, 0, 0.5);
}
.card-title {
text-style: bold;
text-align: center;
color: $text;
}
.card-value {
text-align: center;
color: $text-muted;
}
```

View File

@@ -0,0 +1,241 @@
# Common Widgets
## Display Widgets
### Label / Static
Display static or updatable text:
```python
from textual.widgets import Label, Static
# Label is just an alias for Static
label = Label("Hello World")
static = Static("Initial text")
# Update later
static.update("New text")
static.update("[bold]Rich markup[/bold]")
```
### Placeholder
Useful for prototyping layouts:
```python
from textual.widgets import Placeholder
# Shows widget ID and size info
yield Placeholder("Custom label", id="p1")
yield Placeholder(variant="size") # Shows dimensions
yield Placeholder(variant="text") # Shows placeholder text
```
## Input Widgets
### Button
```python
from textual.widgets import Button
yield Button("Click Me", id="my-button")
yield Button("Disabled", disabled=True)
# Handle click
def on_button_pressed(self, event: Button.Pressed) -> None:
button_id = event.button.id
self.notify(f"{button_id} clicked!")
```
### Input
Single-line text input:
```python
from textual.widgets import Input
yield Input(placeholder="Enter text...", id="name-input")
def on_input_changed(self, event: Input.Changed) -> None:
self.text_value = event.value
def on_input_submitted(self, event: Input.Submitted) -> None:
# User pressed Enter
self.process_input(event.value)
```
### TextArea
Multi-line text editor:
```python
from textual.widgets import TextArea
text_area = TextArea()
text_area.load_text("Initial content")
# Get content
content = text_area.text
```
### Switch
Toggle switch (like checkbox):
```python
from textual.widgets import Switch
yield Switch(value=True) # Initially on
def on_switch_changed(self, event: Switch.Changed) -> None:
is_on = event.value
self.toggle_feature(is_on)
```
## Data Display
### DataTable
Display tabular data:
```python
from textual.widgets import DataTable
table = DataTable()
# Add columns
table.add_columns("Name", "Age", "Country")
# Add rows
table.add_row("Alice", 30, "USA")
table.add_row("Bob", 25, "UK")
# Add row with custom label
from rich.text import Text
label = Text("1", style="bold cyan")
table.add_row("Charlie", 35, "Canada", label=label)
# Configuration
table.zebra_stripes = True # Alternating row colors
table.cursor_type = "row" # "cell", "row", "column", or "none"
table.show_header = True
table.show_row_labels = True
# Handle selection
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
row_key = event.row_key
row_data = table.get_row(row_key)
def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None:
value = event.value
coordinate = event.coordinate
```
## Layout Containers
### Container
Generic container for grouping widgets:
```python
from textual.containers import Container
with Container(id="sidebar"):
yield Label("Title")
yield Button("Action")
```
### Vertical / Horizontal / VerticalScroll / HorizontalScroll
Directional containers:
```python
from textual.containers import Vertical, Horizontal, VerticalScroll
with Horizontal():
yield Button("Left")
yield Button("Right")
with VerticalScroll():
for i in range(100):
yield Label(f"Item {i}")
```
### Grid
Grid layout container:
```python
from textual.containers import Grid
with Grid(id="my-grid"):
yield Label("A")
yield Label("B")
yield Label("C")
yield Label("D")
# Style in CSS:
# Grid {
# grid-size: 2 2; /* 2 columns, 2 rows */
# }
```
## App Widgets
### Header / Footer
Standard app chrome:
```python
from textual.widgets import Header, Footer
def compose(self) -> ComposeResult:
yield Header()
# ... content ...
yield Footer()
```
Footer automatically shows key bindings defined in BINDINGS.
## Custom Widgets
Create reusable components:
```python
from textual.widget import Widget
from textual.widgets import Label, Button
class Card(Widget):
"""A card widget with title and content."""
DEFAULT_CSS = """
Card {
width: 30;
height: 15;
border: round white;
padding: 1;
}
"""
def __init__(self, title: str, content: str) -> None:
super().__init__()
self.title = title
self.content = content
def compose(self) -> ComposeResult:
yield Label(self.title, classes="card-title")
yield Label(self.content, classes="card-content")
yield Button("Select", id=f"select-{self.title}")
```
### Render Method
For simple custom widgets that just render text:
```python
from textual.widget import Widget
class FizzBuzz(Widget):
def render(self) -> str:
return "FizzBuzz!"
```