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,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
```