mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
ko_fi: textualize
|
|
||||||
|
|||||||
18
.github/workflows/pythonpackage.yml
vendored
18
.github/workflows/pythonpackage.yml
vendored
@@ -7,8 +7,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
python-version: ["3.7", "3.8", "3.9"]
|
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
version: 1.1.6
|
version: 1.1.6
|
||||||
virtualenvs-in-project: true
|
virtualenvs-in-project: true
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: poetry install
|
run: poetry install --extras "dev"
|
||||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||||
- name: Format check with black
|
- name: Format check with black
|
||||||
run: |
|
run: |
|
||||||
@@ -39,11 +39,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
source $VENV
|
source $VENV
|
||||||
pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
|
pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
|
||||||
- name: Upload code coverage
|
- name: Upload snapshot report
|
||||||
uses: codecov/codecov-action@v1.0.10
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
name: snapshot-report-textual
|
||||||
file: ./coverage.xml
|
path: tests/snapshot_tests/output/snapshot_report.html
|
||||||
name: rich
|
|
||||||
flags: unittests
|
|
||||||
env_vars: OS,PYTHON
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
.pytype
|
.pytype
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
mypy_report
|
mypy_report
|
||||||
docs/build
|
docs/build
|
||||||
docs/source/_build
|
docs/source/_build
|
||||||
@@ -112,3 +113,6 @@ venv.bak/
|
|||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Snapshot testing report output directory
|
||||||
|
tests/snapshot_tests/output
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.2.0
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
args: ['--unsafe']
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 21.8b0
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
exclude: ^tests/
|
||||||
|
exclude: ^tests/snapshot_tests
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [0.2.0] - 2022-10-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- CSS support
|
||||||
|
- Too numerous to mention
|
||||||
## [0.1.18] - 2022-04-30
|
## [0.1.18] - 2022-04-30
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -1,8 +1,18 @@
|
|||||||
test:
|
test:
|
||||||
pytest --cov-report term-missing --cov=textual tests/ -vv
|
pytest --cov-report term-missing --cov=textual tests/ -vv
|
||||||
|
unit-test:
|
||||||
|
pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test"
|
||||||
|
test-snapshot-update:
|
||||||
|
pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update
|
||||||
typecheck:
|
typecheck:
|
||||||
mypy src/textual
|
mypy src/textual
|
||||||
format:
|
format:
|
||||||
black src
|
black src
|
||||||
format-check:
|
format-check:
|
||||||
black --check .
|
black --check src
|
||||||
|
docs-serve:
|
||||||
|
mkdocs serve
|
||||||
|
docs-build:
|
||||||
|
mkdocs build
|
||||||
|
docs-deploy:
|
||||||
|
mkdocs gh-deploy
|
||||||
|
|||||||
422
README.md
422
README.md
@@ -1,375 +1,121 @@
|
|||||||
# Textual
|
# Textual
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Textual is a TUI (Text User Interface) framework for Python inspired by modern web development. Currently a **Work in Progress**.
|
Textual is a Python framework for creating interactive applications that run in your terminal.
|
||||||
|
|
||||||
> **Warning**
|
|
||||||
>
|
|
||||||
> We ([Textualize.io](https://www.textualize.io)) are hard at work on the **css** branch. We will maintain the 0.1.0 branch for the near future but may not be able to accept API changes. If you would like to contribute code via a PR, please raise a discussion first, to avoid disappointment.
|
|
||||||
|
|
||||||
|
|
||||||
Follow [@willmcgugan](https://twitter.com/willmcgugan) for progress updates, or post in Discussions if you have any requests / suggestions.
|
## About
|
||||||
|
|
||||||
[](https://gitter.im/textual-ui/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with a Python API inspired by modern web development.
|
||||||
|
|
||||||
|
On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
Textual currently runs on **MacOS / Linux / Windows**.
|
Textual runs on Linux, macOS, and Windows. Textual requires Python 3.7 or above.
|
||||||
|
|
||||||
## How it works
|
## Installing
|
||||||
|
|
||||||
Textual uses [Rich](https://github.com/willmcgugan/rich) to render rich text, so anything that Rich can render may be used in Textual.
|
Install Textual via pip:
|
||||||
|
|
||||||
Event handling in Textual is asynchronous (using `async` and `await` keywords). Widgets (UI components) can independently update and communicate with each other via message passing.
|
|
||||||
|
|
||||||
Textual has more in common with modern web development than it does with [curses](<https://en.wikipedia.org/wiki/Curses_(programming_library)>); layout is done with CSS grid and (soon) the theme may be customized with CSS. Other techniques are borrowed from JS frameworks such as Vue and React.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
You can install Textual via pip (`pip install textual`), or by checking out the repo and installing with [poetry](https://python-poetry.org/docs/).
|
|
||||||
|
|
||||||
```
|
```
|
||||||
poetry install
|
pip install textual[dev]
|
||||||
```
|
```
|
||||||
|
|
||||||
Once installed you can run the following command for a quick test, or see examples (below):
|
The addition of `[dev]` installs Textual development tools. See the [docs](https://textual.textualize.io/getting_started/) if you need help getting started.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
Run the following command to see a little of what Textual can do:
|
||||||
|
|
||||||
```
|
```
|
||||||
python -m textual.app
|
python -m textual
|
||||||
```
|
```
|
||||||
|
|
||||||
Textual requires Python 3.7 or above.
|

|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Head over to the [Textual documentation](http://textual.textualize.io/) to start building!
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Until I've written the documentation, the [examples](https://github.com/willmcgugan/textual/tree/main/examples/) may be the best way to learn Textual.
|
The Textual repository comes with a number of examples you can experiment with or use as a template for your own projects.
|
||||||
|
|
||||||
You can see some of these examples in action in the [Developer Video Log](#developer-video-log).
|
|
||||||
|
|
||||||
- [animation.py](https://github.com/willmcgugan/textual/tree/main/examples/animation.py) Demonstration of 60fps animation easing function
|
|
||||||
- [calculator.py](https://github.com/willmcgugan/textual/tree/main/examples/calculator.py) A "clone" of the MacOS calculator using Grid layout
|
|
||||||
- [code_viewer.py](https://github.com/willmcgugan/textual/tree/main/examples/code_viewer.py) A demonstration of a tree view which loads syntax highlighted code
|
|
||||||
- [grid.py](https://github.com/willmcgugan/textual/tree/main/examples/grid.py) A simple demonstration of adding widgets in a Grid layout
|
|
||||||
- [grid_auto.py](https://github.com/willmcgugan/textual/tree/main/examples/grid_auto.py) A demonstration of automatic Grid layout
|
|
||||||
- [simple.py](https://github.com/willmcgugan/textual/tree/main/examples/simple.py) A very simple Textual app with scrolling Markdown view
|
|
||||||
|
|
||||||
## Building Textual applications
|
|
||||||
|
|
||||||
_This guide is a work in progress_
|
|
||||||
|
|
||||||
Let's look at the simplest Textual app which does _something_:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from textual.app import App
|
|
||||||
|
|
||||||
|
|
||||||
class Beeper(App):
|
<details>
|
||||||
def on_key(self):
|
<summary> 🎬 Code browser </summary>
|
||||||
self.console.bell()
|
<hr>
|
||||||
|
|
||||||
|
This is the [code_browser.py](./examples/code_browser.py) example which clocks in at 61 lines (*including* docstrings and blank lines).
|
||||||
|
|
||||||
|
https://user-images.githubusercontent.com/554369/197188237-88d3f7e4-4e5f-40b5-b996-c47b19ee2f49.mov
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
Beeper.run()
|
<details>
|
||||||
|
<summary> 📷 Calculator </summary>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
This is [calculator.py](./examples/calculator.py) which demonstrates Textual grid layouts.
|
||||||
|
|
||||||
|

|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> 📷 Stopwatch </summary>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
This is the Stopwatch example from the tutorial.
|
||||||
|
|
||||||
|
### Light theme
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Dark theme
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Reference commands
|
||||||
|
|
||||||
|
The `textual` command has a few sub-commands to preview Textual styles.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> 🎬 Easing reference </summary>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
This is the *easing* reference which demonstrates the easing parameter on animation, with both movement and opacity. You can run it with the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
textual easing
|
||||||
```
|
```
|
||||||
|
|
||||||
Here we can see a textual app with a single `on_key` method which will handle key events. Pressing any key will result in playing the terminal bell (generally an irritating beep). Hit Ctrl+C to exit.
|
|
||||||
|
|
||||||
Event handlers in Textual are defined by convention, not by inheritance (there's no base class with all the handlers defined). Each event has a `name` attribute which for the key event is simply `"key"`. Textual will call the method named `on_<event.name>` if it exists.
|
https://user-images.githubusercontent.com/554369/196157100-352852a6-2b09-4dc8-a888-55b53570aff9.mov
|
||||||
|
|
||||||
Let's look at a _slightly_ more interesting example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from textual.app import App
|
|
||||||
|
|
||||||
|
|
||||||
class ColorChanger(App):
|
</details>
|
||||||
def on_key(self, event):
|
|
||||||
if event.key.isdigit():
|
|
||||||
self.background = f"on color({event.key})"
|
|
||||||
|
|
||||||
|
<details>
|
||||||
ColorChanger.run(log="textual.log")
|
<summary> 🎬 Borders reference </summary>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
This is the borders reference which demonstrates some of the borders styles in Textual. You can run it with the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
textual borders
|
||||||
```
|
```
|
||||||
|
|
||||||
You'll notice that the `on_key` method above contains an additional `event` parameter which wasn't present on the beeper example. If the `event` argument is present, Textual will call the handler with an event object. Every event has an associated handler object, in this case it is a KeyEvent which contains additional information regarding which key was pressed.
|
|
||||||
|
|
||||||
The key event handler above will set the background attribute if you press the keys 0-9, which turns the terminal to the corresponding [ansi color](https://rich.readthedocs.io/en/latest/appendix/colors.html).
|
https://user-images.githubusercontent.com/554369/196158235-4b45fb78-053d-4fd5-b285-e09b4f1c67a8.mov
|
||||||
|
|
||||||
Note that we didn't need to explicitly refresh the screen or draw anything. Setting the `background` attribute to a [Rich style](https://rich.readthedocs.io/en/latest/style.html) is enough for Textual to update the visuals. This is an example of _reactivity_ in Textual. To make changes to the terminal interface you modify the _state_ and let Textual update the UI.
|
|
||||||
|
|
||||||
## Widgets
|
|
||||||
|
</details>
|
||||||
To make more interesting apps you will need to make use of _widgets_, which are independent user interface elements. Textual comes with a (growing) library of widgets, but you can develop your own.
|
|
||||||
|
|
||||||
Let's look at an app which contains widgets. We will be using the built-in `Placeholder` widget which you can use to design application layouts before you implement the real content.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from textual.app import App
|
|
||||||
from textual.widgets import Placeholder
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleApp(App):
|
|
||||||
|
|
||||||
async def on_mount(self) -> None:
|
|
||||||
await self.view.dock(Placeholder(), edge="left", size=40)
|
|
||||||
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
|
||||||
|
|
||||||
|
|
||||||
SimpleApp.run(log="textual.log")
|
|
||||||
```
|
|
||||||
|
|
||||||
This app contains a single event handler `on_mount`. The mount event is sent when the app or widget is ready to start processing events, and is typically used for initialization. You may have noticed that `on_mount` is an `async` function. Since Textual is an asynchronous framework we will need this if we need to call most other methods.
|
|
||||||
|
|
||||||
The `on_mount` method makes two calls to `self.view.dock` which adds widgets to the terminal.
|
|
||||||
|
|
||||||
Here's the first line in the mount handler:
|
|
||||||
|
|
||||||
```python
|
|
||||||
await self.view.dock(Placeholder(), edge="left", size=40)
|
|
||||||
```
|
|
||||||
|
|
||||||
Note this method is asynchronous like almost all API methods in Textual. We are awaiting `self.view.dock` which takes a newly constructed Placeholder widget, and docks it on to the `"left"` edge of the terminal with a size of 40 characters. In a real app you might use this to display a side-bar.
|
|
||||||
|
|
||||||
The following line is similar:
|
|
||||||
|
|
||||||
```python
|
|
||||||
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
|
||||||
```
|
|
||||||
|
|
||||||
You will notice that this time we are docking _two_ Placeholder objects onto the `"top"` edge. We haven't set an explicit size this time so Textual will divide the remaining size amongst the two new widgets.
|
|
||||||
|
|
||||||
The last line calls the `run` class method in the usual way, but with an argument we haven't seen before: `log="textual.log"` tells Textual to write log information to the given file. You can tail textual.log to see events being processed and other debug information.
|
|
||||||
|
|
||||||
If you run the above example, you will see something like the following:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If you move the mouse over the terminal you will notice that widgets receive mouse events. You can click any of the placeholders to give it input focus.
|
|
||||||
|
|
||||||
The dock layout feature is very flexible, but for more sophisticated layouts we can use the grid API. See the [calculator.py](https://github.com/willmcgugan/textual/blob/main/examples/calculator.py) example which makes use of Grid.
|
|
||||||
|
|
||||||
### Creating Widgets
|
|
||||||
|
|
||||||
You can create your own widgets by subclassing the `textual.widget.Widget` class and implementing a `render()` method which should return anything that can be rendered with [Rich](https://rich.readthedocs.io/en/latest/introduction.html), including a plain string which will be interpreted as [console markup](https://rich.readthedocs.io/en/latest/markup.html).
|
|
||||||
|
|
||||||
Let's look at an example with a custom widget:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from rich.panel import Panel
|
|
||||||
|
|
||||||
from textual.app import App
|
|
||||||
from textual.reactive import Reactive
|
|
||||||
from textual.widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class Hover(Widget):
|
|
||||||
|
|
||||||
mouse_over = Reactive(False)
|
|
||||||
|
|
||||||
def render(self) -> Panel:
|
|
||||||
return Panel("Hello [b]World[/b]", style=("on red" if self.mouse_over else ""))
|
|
||||||
|
|
||||||
def on_enter(self) -> None:
|
|
||||||
self.mouse_over = True
|
|
||||||
|
|
||||||
def on_leave(self) -> None:
|
|
||||||
self.mouse_over = False
|
|
||||||
|
|
||||||
|
|
||||||
class HoverApp(App):
|
|
||||||
"""Demonstrates custom widgets"""
|
|
||||||
|
|
||||||
async def on_mount(self) -> None:
|
|
||||||
hovers = (Hover() for _ in range(10))
|
|
||||||
await self.view.dock(*hovers, edge="top")
|
|
||||||
|
|
||||||
|
|
||||||
HoverApp.run(log="textual.log")
|
|
||||||
```
|
|
||||||
|
|
||||||
The `Hover` class is a custom widget which displays a panel containing the classic text "Hello World". The first line in the Hover class may seem a little mysterious at this point:
|
|
||||||
|
|
||||||
```python
|
|
||||||
mouse_over = Reactive(False)
|
|
||||||
```
|
|
||||||
|
|
||||||
This adds a `mouse_over` attribute to your class which is a bool with a default of `False`. Adding attributes like this makes them _reactive_: any changes will result in the widget updating.
|
|
||||||
|
|
||||||
The following `render()` method is where you define how the widget should be displayed. In the Hover widget we return a [Panel](https://rich.readthedocs.io/en/latest/panel.html) containing rich text with a background that changes depending on the value of `mouse_over`. The goal here is to add a mouse hover effect to the widget, which we can achieve by handling two events: `Enter` and `Leave`. These events are sent when the mouse enters or leaves the widget.
|
|
||||||
|
|
||||||
Here are the two event handlers again:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def on_enter(self) -> None:
|
|
||||||
self.mouse_over = True
|
|
||||||
|
|
||||||
def on_leave(self) -> None:
|
|
||||||
self.mouse_over = False
|
|
||||||
```
|
|
||||||
|
|
||||||
Both event handlers set the `mouse_over` attribute which will result in the widget's `render()` method being called.
|
|
||||||
|
|
||||||
The `HoverApp` has a `on_mount` handler which creates 10 Hover widgets and docks them on the top edge to create a vertical stack:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def on_mount(self) -> None:
|
|
||||||
hovers = (Hover() for _ in range(10))
|
|
||||||
await self.view.dock(*hovers, edge="top")
|
|
||||||
```
|
|
||||||
|
|
||||||
If you run this script you will see something like the following:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If you move your mouse over the terminal you should see that the widget under the mouse cursor changes to a red background.
|
|
||||||
|
|
||||||
### Actions and key bindings
|
|
||||||
|
|
||||||
Actions in Textual are white-listed functions that may be bound to keys. Let's look at a trivial example of binding a key to an action. Here is an app which exits when we hit the Q key:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from textual.app import App
|
|
||||||
|
|
||||||
|
|
||||||
class Quitter(App):
|
|
||||||
async def on_load(self, event):
|
|
||||||
await self.bind("q", "quit")
|
|
||||||
|
|
||||||
|
|
||||||
Quitter.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
If you run this you will get a blank terminal which will return to the prompt when you press Q.
|
|
||||||
|
|
||||||
Binding is done in the Load event handler. The `bind` method takes the key (in this case "q") and binds it to an action ("quit"). The quit action is built in to Textual and simply exits the app.
|
|
||||||
|
|
||||||
To define your own actions, add a method that begins with `action_`, which may take parameters. Let's create a simple action that changes the color of the terminal and binds keys to it:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from textual.app import App
|
|
||||||
|
|
||||||
|
|
||||||
class Colorizer(App):
|
|
||||||
|
|
||||||
async def on_load(self, event):
|
|
||||||
await self.bind("r", "color('red')")
|
|
||||||
await self.bind("g", "color('green')")
|
|
||||||
await self.bind("b", "color('blue')")
|
|
||||||
|
|
||||||
async def action_color(self, color:str) -> None:
|
|
||||||
self.background = f"on {color}"
|
|
||||||
|
|
||||||
|
|
||||||
Colorizer.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
If you run this app you can hit the keys R, G, or B to change the color of the background.
|
|
||||||
|
|
||||||
In the `on_load` method we have bound the keys R, G, and B to the `color` action with a single parameter. When you press any of these three keys Textual will call the method `action_color` with the appropriate parameter.
|
|
||||||
|
|
||||||
You could be forgiven for thinking that `"color('red')"` is Python code which Textual evaluates. This is not the case. The action strings are parsed and may not include expressions or arbitrary code. The reason that strings are used over a callable is that (in a future update) key bindings may be loaded from a configuration file.
|
|
||||||
|
|
||||||
### More on Events
|
|
||||||
|
|
||||||
_TODO_
|
|
||||||
|
|
||||||
### Watchers
|
|
||||||
|
|
||||||
_TODO_
|
|
||||||
|
|
||||||
### Animation
|
|
||||||
|
|
||||||
_TODO_
|
|
||||||
|
|
||||||
### Timers and Intervals
|
|
||||||
|
|
||||||
Textual has a `set_timer` and a `set_interval` method which work much like their Javascript counterparts. The `set_timer` method will invoke a callable after a given period of time, and `set_interval` will invoke a callable repeatedly. Unlike Javascript these methods expect the time to be in seconds (_not_ milliseconds).
|
|
||||||
|
|
||||||
Let's create a simple terminal based clock with the `set_interval` method:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from rich.align import Align
|
|
||||||
|
|
||||||
from textual.app import App
|
|
||||||
from textual.widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class Clock(Widget):
|
|
||||||
def on_mount(self):
|
|
||||||
self.set_interval(1, self.refresh)
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
time = datetime.now().strftime("%c")
|
|
||||||
return Align.center(time, vertical="middle")
|
|
||||||
|
|
||||||
|
|
||||||
class ClockApp(App):
|
|
||||||
async def on_mount(self):
|
|
||||||
await self.view.dock(Clock())
|
|
||||||
|
|
||||||
|
|
||||||
ClockApp.run()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
If you run this app you will see the current time in the center of the terminal until you hit Ctrl+C.
|
|
||||||
|
|
||||||
The Clock widget displays the time using [rich.align.Align](https://rich.readthedocs.io/en/latest/reference/align.html) to position it in the center. In the clock's Mount handler there is the following call to `set_interval`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
self.set_interval(1, self.refresh)
|
|
||||||
```
|
|
||||||
|
|
||||||
This tells Textual to call a function (in this case `self.refresh` which updates the widget) once a second. When a widget is refreshed it calls `Clock.render` again to display the latest time.
|
|
||||||
|
|
||||||
## Developer Video Log
|
|
||||||
|
|
||||||
Since Textual is a visual medium, I'll be documenting new features and milestones here.
|
|
||||||
|
|
||||||
### Update 1 - Basic scrolling
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=zNW7U36GHlU)
|
|
||||||
|
|
||||||
### Update 2 - Keyboard toggle
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=bTYeFOVNXDI)
|
|
||||||
|
|
||||||
### Update 3 - New scrollbars and smooth scrolling
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=4LVl3ClrXIs)
|
|
||||||
|
|
||||||
### Update 4 - Animation system with easing function
|
|
||||||
|
|
||||||
Now with a system to animate changes to values, going from the initial to the final value in small increments over time . Here applied to the scroll position. The animation system supports CSS like _easing functions_. You may be able to tell from the video that the page up / down keys cause the window to first speed up and then slow down.
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=k2VwOp1YbSk)
|
|
||||||
|
|
||||||
### Update 5 - New Layout system
|
|
||||||
|
|
||||||
A new update system allows for overlapping layers. Animation is now synchronized with the display which makes it very smooth!
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=XxRnfx2WYRw)
|
|
||||||
|
|
||||||
### Update 6 - New Layout API
|
|
||||||
|
|
||||||
New version (0.1.4) with API updates and the new layout system.
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=jddccDuVd3E)
|
|
||||||
|
|
||||||
### Update 7 - New Grid Layout
|
|
||||||
|
|
||||||
**11 July 2021**
|
|
||||||
|
|
||||||
Added a new layout system modelled on CSS grid. The example demonstrates how once created a grid will adapt to the available space.
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=Zh9CEvu73jc)
|
|
||||||
|
|
||||||
## Update 8 - Tree control and scroll views
|
|
||||||
|
|
||||||
**6 Aug 2021**
|
|
||||||
|
|
||||||
Added a tree control and refactored the renderer to allow for widgets within a scrollable view
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=J-dzzD6NQJ4)
|
|
||||||
|
|||||||
18
docs.md
Normal file
18
docs.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Documentation Workflow
|
||||||
|
|
||||||
|
* Ensure you're inside a *Python 3.10+* virtual environment
|
||||||
|
* Run the live-reload server using `mkdocs serve` from the project root
|
||||||
|
* Create new pages by adding new directories and Markdown files inside `docs/*`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `mkdocs serve` - Start the live-reloading docs server.
|
||||||
|
- `mkdocs build` - Build the documentation site.
|
||||||
|
- `mkdocs -h` - Print help message and exit.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
mkdocs.yml # The configuration file.
|
||||||
|
docs/
|
||||||
|
index.md # The documentation homepage.
|
||||||
|
... # Other markdown pages, images and other files.
|
||||||
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
|||||||
|
textual.textualize.io
|
||||||
8
docs/custom_theme/main.html
Normal file
8
docs/custom_theme/main.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/fira_code.min.css" integrity="sha512-MbysAYimH1hH2xYzkkMHB6MqxBqfP0megxsCLknbYqHVwXTCg9IqHbk+ZP/vnhO8UEW6PaXAkKe2vQ+SWACxxA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<!-- Fathom - beautiful, simple website analytics -->
|
||||||
|
<script src="https://cdn.usefathom.com/script.js" data-site="TAUKXRLQ" defer></script>
|
||||||
|
<!-- / Fathom -->
|
||||||
|
{% endblock %}
|
||||||
14
docs/events/blur.md
Normal file
14
docs/events/blur.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Blur
|
||||||
|
|
||||||
|
The `Blur` event is sent to a widget when it loses focus.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Blur
|
||||||
25
docs/events/click.md
Normal file
25
docs/events/click.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Click
|
||||||
|
|
||||||
|
The `Click` event is sent to a widget when the user clicks a mouse button.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|------------|------|-------------------------------------------|
|
||||||
|
| `x` | int | Mouse x coordinate, relative to Widget |
|
||||||
|
| `y` | int | Mouse y coordinate, relative to Widget |
|
||||||
|
| `delta_x` | int | Change in x since last mouse event |
|
||||||
|
| `delta_y` | int | Change in y since last mouse event |
|
||||||
|
| `button` | int | Index of mouse button |
|
||||||
|
| `shift` | bool | Shift key pressed if True |
|
||||||
|
| `meta` | bool | Meta key pressed if True |
|
||||||
|
| `ctrl` | bool | Ctrl key pressed if True |
|
||||||
|
| `screen_x` | int | Mouse x coordinate relative to the screen |
|
||||||
|
| `screen_y` | int | Mouse y coordinate relative to the screen |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Click
|
||||||
14
docs/events/descendant_blur.md
Normal file
14
docs/events/descendant_blur.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# DescendantBlur
|
||||||
|
|
||||||
|
The `DescendantBlur` event is sent to a widget when one of its children loses focus.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.DescendantBlur
|
||||||
14
docs/events/descendant_focus.md
Normal file
14
docs/events/descendant_focus.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# DescendantFocus
|
||||||
|
|
||||||
|
The `DescendantFocus` event is sent to a widget when one of its descendants receives focus.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.DescendantFocus
|
||||||
14
docs/events/enter.md
Normal file
14
docs/events/enter.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Enter
|
||||||
|
|
||||||
|
The `Enter` event is sent to a widget when the mouse pointer first moves over a widget.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Enter
|
||||||
14
docs/events/focus.md
Normal file
14
docs/events/focus.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Focus
|
||||||
|
|
||||||
|
The `Focus` event is sent to a widget when it receives input focus.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Focus
|
||||||
14
docs/events/hide.md
Normal file
14
docs/events/hide.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Hide
|
||||||
|
|
||||||
|
The `Hide` event is sent to a widget when it is hidden from view.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No additional attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Hide
|
||||||
3
docs/events/index.md
Normal file
3
docs/events/index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Events
|
||||||
|
|
||||||
|
A reference to Textual [events](../guide/events.md).
|
||||||
17
docs/events/key.md
Normal file
17
docs/events/key.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Key
|
||||||
|
|
||||||
|
The `Key` event is sent to a widget when the user presses a key on the keyboard.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
| --------- | ----------- | ----------------------------------------------------------- |
|
||||||
|
| `key` | str | Name of the key that was pressed. |
|
||||||
|
| `char` | str or None | The character that was pressed, or None it isn't printable. |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Key
|
||||||
14
docs/events/leave.md
Normal file
14
docs/events/leave.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Leave
|
||||||
|
|
||||||
|
The `Leave` event is sent to a widget when the mouse pointer moves off a widget.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Leave
|
||||||
16
docs/events/load.md
Normal file
16
docs/events/load.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Load
|
||||||
|
|
||||||
|
The `Load` event is sent to the app prior to switching the terminal to application mode.
|
||||||
|
|
||||||
|
The load event is typically used to do any setup actions required by the app that don't change the display.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No additional attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Load
|
||||||
16
docs/events/mount.md
Normal file
16
docs/events/mount.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Mount
|
||||||
|
|
||||||
|
The `Mount` event is sent to a widget and Application when it is first mounted.
|
||||||
|
|
||||||
|
The mount event is typically used to set the initial state of a widget or to add new children widgets.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No additional attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Mount
|
||||||
16
docs/events/mouse_capture.md
Normal file
16
docs/events/mouse_capture.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# MouseCapture
|
||||||
|
|
||||||
|
The `MouseCapture` event is sent to a widget when it is capturing mouse events from outside of its borders on the screen.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
| ---------------- | ------ | --------------------------------------------- |
|
||||||
|
| `mouse_position` | Offset | Mouse coordinates when the mouse was captured |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.MouseCapture
|
||||||
25
docs/events/mouse_down.md
Normal file
25
docs/events/mouse_down.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# MouseDown
|
||||||
|
|
||||||
|
The `MouseDown` event is sent to a widget when a mouse button is pressed.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
| ---------- | ---- | ----------------------------------------- |
|
||||||
|
| `x` | int | Mouse x coordinate, relative to Widget |
|
||||||
|
| `y` | int | Mouse y coordinate, relative to Widget |
|
||||||
|
| `delta_x` | int | Change in x since last mouse event |
|
||||||
|
| `delta_y` | int | Change in y since last mouse event |
|
||||||
|
| `button` | int | Index of mouse button |
|
||||||
|
| `shift` | bool | Shift key pressed if True |
|
||||||
|
| `meta` | bool | Meta key pressed if True |
|
||||||
|
| `ctrl` | bool | Ctrl key pressed if True |
|
||||||
|
| `screen_x` | int | Mouse x coordinate relative to the screen |
|
||||||
|
| `screen_y` | int | Mouse y coordinate relative to the screen |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.MouseDown
|
||||||
25
docs/events/mouse_move.md
Normal file
25
docs/events/mouse_move.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# MouseMove
|
||||||
|
|
||||||
|
The `MouseMove` event is sent to a widget when the mouse pointer is moved over a widget.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|------------|------|-------------------------------------------|
|
||||||
|
| `x` | int | Mouse x coordinate, relative to Widget |
|
||||||
|
| `y` | int | Mouse y coordinate, relative to Widget |
|
||||||
|
| `delta_x` | int | Change in x since last mouse event |
|
||||||
|
| `delta_y` | int | Change in y since last mouse event |
|
||||||
|
| `button` | int | Index of mouse button |
|
||||||
|
| `shift` | bool | Shift key pressed if True |
|
||||||
|
| `meta` | bool | Meta key pressed if True |
|
||||||
|
| `ctrl` | bool | Ctrl key pressed if True |
|
||||||
|
| `screen_x` | int | Mouse x coordinate relative to the screen |
|
||||||
|
| `screen_y` | int | Mouse y coordinate relative to the screen |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.MouseMove
|
||||||
16
docs/events/mouse_release.md
Normal file
16
docs/events/mouse_release.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# MouseRelease
|
||||||
|
|
||||||
|
The `MouseRelease` event is sent to a widget when it is no longer receiving mouse events outside of its borders.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|------------------|--------|-----------------------------------------------|
|
||||||
|
| `mouse_position` | Offset | Mouse coordinates when the mouse was released |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.MouseRelease
|
||||||
17
docs/events/mouse_scroll_down.md
Normal file
17
docs/events/mouse_scroll_down.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# MouseScrollDown
|
||||||
|
|
||||||
|
The `MouseScrollDown` event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved _down_.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|-----------|------|----------------------------------------|
|
||||||
|
| `x` | int | Mouse x coordinate, relative to Widget |
|
||||||
|
| `y` | int | Mouse y coordinate, relative to Widget |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.MouseScrollDown
|
||||||
17
docs/events/mouse_scroll_up.md
Normal file
17
docs/events/mouse_scroll_up.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# MouseScrollUp
|
||||||
|
|
||||||
|
The `MouseScrollUp` event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved _up_.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|-----------|------|----------------------------------------|
|
||||||
|
| `x` | int | Mouse x coordinate, relative to Widget |
|
||||||
|
| `y` | int | Mouse y coordinate, relative to Widget |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.MouseScrollUp
|
||||||
25
docs/events/mouse_up.md
Normal file
25
docs/events/mouse_up.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# MouseUp
|
||||||
|
|
||||||
|
The `MouseUp` event is sent to a widget when the user releases a mouse button.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [x] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|------------|------|-------------------------------------------|
|
||||||
|
| `x` | int | Mouse x coordinate, relative to Widget |
|
||||||
|
| `y` | int | Mouse y coordinate, relative to Widget |
|
||||||
|
| `delta_x` | int | Change in x since last mouse event |
|
||||||
|
| `delta_y` | int | Change in y since last mouse event |
|
||||||
|
| `button` | int | Index of mouse button |
|
||||||
|
| `shift` | bool | Shift key pressed if True |
|
||||||
|
| `meta` | bool | Meta key pressed if True |
|
||||||
|
| `ctrl` | bool | Ctrl key pressed if True |
|
||||||
|
| `screen_x` | int | Mouse x coordinate relative to the screen |
|
||||||
|
| `screen_y` | int | Mouse y coordinate relative to the screen |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.MouseUp
|
||||||
16
docs/events/paste.md
Normal file
16
docs/events/paste.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Paste
|
||||||
|
|
||||||
|
The `Paste` event is sent to a widget when the user pastes text.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|-----------|------|--------------------------|
|
||||||
|
| `text` | str | The text that was pasted |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Paste
|
||||||
18
docs/events/resize.md
Normal file
18
docs/events/resize.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Resize
|
||||||
|
|
||||||
|
The `Resize` event is sent to a widget when its size changes and when it is first made visible.
|
||||||
|
|
||||||
|
- [x] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
| attribute | type | purpose |
|
||||||
|
|------------------|------|--------------------------------------------------|
|
||||||
|
| `size` | Size | The new size of the Widget |
|
||||||
|
| `virtual_size` | Size | The virtual size (scrollable area) of the Widget |
|
||||||
|
| `container_size` | Size | The size of the container (parent widget) |
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Resize
|
||||||
14
docs/events/screen_resume.md
Normal file
14
docs/events/screen_resume.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ScreenResume
|
||||||
|
|
||||||
|
The `ScreenResume` event is sent to a **Screen** when it becomes current.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.ScreenResume
|
||||||
14
docs/events/screen_suspend.md
Normal file
14
docs/events/screen_suspend.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ScreenSuspend
|
||||||
|
|
||||||
|
The `ScreenSuspend` event is sent to a **Screen** when it is replaced by another screen.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No other attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.ScreenSuspend
|
||||||
14
docs/events/show.md
Normal file
14
docs/events/show.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Show
|
||||||
|
|
||||||
|
The `Show` event is sent to a widget when it becomes visible.
|
||||||
|
|
||||||
|
- [ ] Bubbles
|
||||||
|
- [ ] Verbose
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
_No additional attributes_
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
::: textual.events.Show
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from textual.app import App
|
|
||||||
|
|
||||||
|
|
||||||
class Colorizer(App):
|
|
||||||
async def on_load(self):
|
|
||||||
await self.bind("r", "color('red')")
|
|
||||||
await self.bind("g", "color('green')")
|
|
||||||
await self.bind("b", "color('blue')")
|
|
||||||
|
|
||||||
def action_color(self, color: str) -> None:
|
|
||||||
self.background = f"on {color}"
|
|
||||||
|
|
||||||
|
|
||||||
Colorizer.run()
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from textual.app import App
|
|
||||||
|
|
||||||
|
|
||||||
class Quiter(App):
|
|
||||||
async def on_load(self):
|
|
||||||
await self.bind("q", "quit")
|
|
||||||
|
|
||||||
|
|
||||||
Quiter.run()
|
|
||||||
30
docs/examples/app/event01.py
Normal file
30
docs/examples/app/event01.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual import events
|
||||||
|
|
||||||
|
|
||||||
|
class EventApp(App):
|
||||||
|
|
||||||
|
COLORS = [
|
||||||
|
"white",
|
||||||
|
"maroon",
|
||||||
|
"red",
|
||||||
|
"purple",
|
||||||
|
"fuchsia",
|
||||||
|
"olive",
|
||||||
|
"yellow",
|
||||||
|
"navy",
|
||||||
|
"teal",
|
||||||
|
"aqua",
|
||||||
|
]
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.screen.styles.background = "darkblue"
|
||||||
|
|
||||||
|
def on_key(self, event: events.Key) -> None:
|
||||||
|
if event.key.isdecimal():
|
||||||
|
self.screen.styles.background = self.COLORS[int(event.key)]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = EventApp()
|
||||||
|
app.run()
|
||||||
18
docs/examples/app/question01.py
Normal file
18
docs/examples/app/question01.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static, Button
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionApp(App[str]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("Do you love Textual?")
|
||||||
|
yield Button("Yes", id="yes", variant="primary")
|
||||||
|
yield Button("No", id="no", variant="error")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
self.exit(event.button.id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QuestionApp()
|
||||||
|
reply = app.run()
|
||||||
|
print(reply)
|
||||||
17
docs/examples/app/question02.css
Normal file
17
docs/examples/app/question02.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 2;
|
||||||
|
grid-gutter: 2;
|
||||||
|
padding: 2;
|
||||||
|
}
|
||||||
|
#question {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
column-span: 2;
|
||||||
|
content-align: center bottom;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
20
docs/examples/app/question02.py
Normal file
20
docs/examples/app/question02.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static, Button
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionApp(App[str]):
|
||||||
|
CSS_PATH = "question02.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("Do you love Textual?", id="question")
|
||||||
|
yield Button("Yes", id="yes", variant="primary")
|
||||||
|
yield Button("No", id="no", variant="error")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
self.exit(event.button.id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QuestionApp()
|
||||||
|
reply = app.run()
|
||||||
|
print(reply)
|
||||||
38
docs/examples/app/question03.py
Normal file
38
docs/examples/app/question03.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static, Button
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionApp(App[str]):
|
||||||
|
CSS = """
|
||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 2;
|
||||||
|
grid-gutter: 2;
|
||||||
|
padding: 2;
|
||||||
|
}
|
||||||
|
#question {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
column-span: 2;
|
||||||
|
content-align: center bottom;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("Do you love Textual?", id="question")
|
||||||
|
yield Button("Yes", id="yes", variant="primary")
|
||||||
|
yield Button("No", id="no", variant="error")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
self.exit(event.button.id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QuestionApp()
|
||||||
|
reply = app.run()
|
||||||
|
print(reply)
|
||||||
5
docs/examples/app/simple01.py
Normal file
5
docs/examples/app/simple01.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
|
||||||
|
class MyApp(App):
|
||||||
|
pass
|
||||||
10
docs/examples/app/simple02.py
Normal file
10
docs/examples/app/simple02.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
|
||||||
|
class MyApp(App):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = MyApp()
|
||||||
|
app.run()
|
||||||
15
docs/examples/app/widgets01.py
Normal file
15
docs/examples/app/widgets01.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Welcome
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Welcome()
|
||||||
|
|
||||||
|
def on_button_pressed(self) -> None:
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = WelcomeApp()
|
||||||
|
app.run()
|
||||||
15
docs/examples/app/widgets02.py
Normal file
15
docs/examples/app/widgets02.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual.widgets import Welcome
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeApp(App):
|
||||||
|
def on_key(self) -> None:
|
||||||
|
self.mount(Welcome())
|
||||||
|
|
||||||
|
def on_button_pressed(self) -> None:
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = WelcomeApp()
|
||||||
|
app.run()
|
||||||
48
docs/examples/events/custom01.py
Normal file
48
docs/examples/events/custom01.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.color import Color
|
||||||
|
from textual.message import Message, MessageTarget
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class ColorButton(Static):
|
||||||
|
"""A color button."""
|
||||||
|
|
||||||
|
class Selected(Message):
|
||||||
|
"""Color selected message."""
|
||||||
|
|
||||||
|
def __init__(self, sender: MessageTarget, color: Color) -> None:
|
||||||
|
self.color = color
|
||||||
|
super().__init__(sender)
|
||||||
|
|
||||||
|
def __init__(self, color: Color) -> None:
|
||||||
|
self.color = color
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.styles.margin = (1, 2)
|
||||||
|
self.styles.content_align = ("center", "middle")
|
||||||
|
self.styles.background = Color.parse("#ffffff33")
|
||||||
|
self.styles.border = ("tall", self.color)
|
||||||
|
|
||||||
|
async def on_click(self) -> None:
|
||||||
|
# The emit method sends an event to a widget's parent
|
||||||
|
await self.emit(self.Selected(self, self.color))
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
return str(self.color)
|
||||||
|
|
||||||
|
|
||||||
|
class ColorApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield ColorButton(Color.parse("#008080"))
|
||||||
|
yield ColorButton(Color.parse("#808000"))
|
||||||
|
yield ColorButton(Color.parse("#E9967A"))
|
||||||
|
yield ColorButton(Color.parse("#121212"))
|
||||||
|
|
||||||
|
def on_color_button_selected(self, message: ColorButton.Selected) -> None:
|
||||||
|
self.screen.styles.animate("background", message.color, duration=0.5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ColorApp()
|
||||||
|
app.run()
|
||||||
23
docs/examples/events/dictionary.css
Normal file
23
docs/examples/events/dictionary.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Screen {
|
||||||
|
background: $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
Input {
|
||||||
|
dock: top;
|
||||||
|
width: 100%;
|
||||||
|
height: 1;
|
||||||
|
padding: 0 1;
|
||||||
|
margin: 1 1 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#results {
|
||||||
|
width: auto;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#results-container {
|
||||||
|
background: $background 50%;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 1 2;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
44
docs/examples/events/dictionary.py
Normal file
44
docs/examples/events/dictionary.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("Please install httpx with 'pip install httpx' ")
|
||||||
|
|
||||||
|
from rich.json import JSON
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Vertical
|
||||||
|
from textual.widgets import Static, Input
|
||||||
|
|
||||||
|
|
||||||
|
class DictionaryApp(App):
|
||||||
|
"""Searches ab dictionary API as-you-type."""
|
||||||
|
|
||||||
|
CSS_PATH = "dictionary.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Input(placeholder="Search for a word")
|
||||||
|
yield Vertical(Static(id="results"), id="results-container")
|
||||||
|
|
||||||
|
async def on_input_changed(self, message: Input.Changed) -> None:
|
||||||
|
"""A coroutine to handle a text changed message."""
|
||||||
|
if message.value:
|
||||||
|
# Look up the word in the background
|
||||||
|
asyncio.create_task(self.lookup_word(message.value))
|
||||||
|
else:
|
||||||
|
# Clear the results
|
||||||
|
self.query_one("#results", Static).update()
|
||||||
|
|
||||||
|
async def lookup_word(self, word: str) -> None:
|
||||||
|
"""Looks up a word."""
|
||||||
|
url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
results = (await client.get(url)).text
|
||||||
|
|
||||||
|
if word == self.query_one(Input).value:
|
||||||
|
self.query_one("#results", Static).update(JSON(results))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = DictionaryApp()
|
||||||
|
app.run()
|
||||||
18
docs/examples/getting_started/console.py
Normal file
18
docs/examples/getting_started/console.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Simulates a screenshot of the Textual devtools
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
from textual.devtools.renderables import DevConsoleHeader
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleApp(App):
|
||||||
|
def compose(self):
|
||||||
|
self.dark = True
|
||||||
|
yield Static(DevConsoleHeader())
|
||||||
|
|
||||||
|
|
||||||
|
app = ConsoleApp()
|
||||||
16
docs/examples/guide/actions/actions01.py
Normal file
16
docs/examples/guide/actions/actions01.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual import events
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsApp(App):
|
||||||
|
def action_set_background(self, color: str) -> None:
|
||||||
|
self.screen.styles.background = color
|
||||||
|
|
||||||
|
def on_key(self, event: events.Key) -> None:
|
||||||
|
if event.key == "r":
|
||||||
|
self.action_set_background("red")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ActionsApp()
|
||||||
|
app.run()
|
||||||
16
docs/examples/guide/actions/actions02.py
Normal file
16
docs/examples/guide/actions/actions02.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual import events
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsApp(App):
|
||||||
|
def action_set_background(self, color: str) -> None:
|
||||||
|
self.screen.styles.background = color
|
||||||
|
|
||||||
|
async def on_key(self, event: events.Key) -> None:
|
||||||
|
if event.key == "r":
|
||||||
|
await self.action("set_background('red')")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ActionsApp()
|
||||||
|
app.run()
|
||||||
22
docs/examples/guide/actions/actions03.py
Normal file
22
docs/examples/guide/actions/actions03.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
TEXT = """
|
||||||
|
[b]Set your background[/b]
|
||||||
|
[@click=set_background('red')]Red[/]
|
||||||
|
[@click=set_background('green')]Green[/]
|
||||||
|
[@click=set_background('blue')]Blue[/]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static(TEXT)
|
||||||
|
|
||||||
|
def action_set_background(self, color: str) -> None:
|
||||||
|
self.screen.styles.background = color
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ActionsApp()
|
||||||
|
app.run()
|
||||||
28
docs/examples/guide/actions/actions04.py
Normal file
28
docs/examples/guide/actions/actions04.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
TEXT = """
|
||||||
|
[b]Set your background[/b]
|
||||||
|
[@click=set_background('red')]Red[/]
|
||||||
|
[@click=set_background('green')]Green[/]
|
||||||
|
[@click=set_background('blue')]Blue[/]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsApp(App):
|
||||||
|
BINDINGS = [
|
||||||
|
("r", "set_background('red')", "Red"),
|
||||||
|
("g", "set_background('green')", "Green"),
|
||||||
|
("b", "set_background('blue')", "Blue"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static(TEXT)
|
||||||
|
|
||||||
|
def action_set_background(self, color: str) -> None:
|
||||||
|
self.screen.styles.background = color
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ActionsApp()
|
||||||
|
app.run()
|
||||||
11
docs/examples/guide/actions/actions05.css
Normal file
11
docs/examples/guide/actions/actions05.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 1;
|
||||||
|
grid-gutter: 2 4;
|
||||||
|
grid-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorSwitcher {
|
||||||
|
height: 100%;
|
||||||
|
margin: 2 4;
|
||||||
|
}
|
||||||
35
docs/examples/guide/actions/actions05.py
Normal file
35
docs/examples/guide/actions/actions05.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
TEXT = """
|
||||||
|
[b]Set your background[/b]
|
||||||
|
[@click=set_background('cyan')]Cyan[/]
|
||||||
|
[@click=set_background('magenta')]Magenta[/]
|
||||||
|
[@click=set_background('yellow')]Yellow[/]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ColorSwitcher(Static):
|
||||||
|
def action_set_background(self, color: str) -> None:
|
||||||
|
self.styles.background = color
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsApp(App):
|
||||||
|
CSS_PATH = "actions05.css"
|
||||||
|
BINDINGS = [
|
||||||
|
("r", "set_background('red')", "Red"),
|
||||||
|
("g", "set_background('green')", "Green"),
|
||||||
|
("b", "set_background('blue')", "Blue"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield ColorSwitcher(TEXT)
|
||||||
|
yield ColorSwitcher(TEXT)
|
||||||
|
|
||||||
|
def action_set_background(self, color: str) -> None:
|
||||||
|
self.screen.styles.background = color
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ActionsApp()
|
||||||
|
app.run()
|
||||||
19
docs/examples/guide/animator/animation01.py
Normal file
19
docs/examples/guide/animator/animation01.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class AnimationApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
self.box = Static("Hello, World!")
|
||||||
|
self.box.styles.background = "red"
|
||||||
|
self.box.styles.color = "black"
|
||||||
|
self.box.styles.padding = (1, 2)
|
||||||
|
yield self.box
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
self.box.styles.animate("opacity", value=0.0, duration=2.0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = AnimationApp()
|
||||||
|
app.run()
|
||||||
16
docs/examples/guide/animator/animation01_static.py
Normal file
16
docs/examples/guide/animator/animation01_static.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class AnimationApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
self.box = Static("Hello, World!")
|
||||||
|
self.box.styles.background = "red"
|
||||||
|
self.box.styles.color = "black"
|
||||||
|
self.box.styles.padding = (1, 2)
|
||||||
|
yield self.box
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = AnimationApp()
|
||||||
|
app.run()
|
||||||
10
docs/examples/guide/dom1.py
Normal file
10
docs/examples/guide/dom1.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleApp(App):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ExampleApp()
|
||||||
|
app.run()
|
||||||
13
docs/examples/guide/dom2.py
Normal file
13
docs/examples/guide/dom2.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Header, Footer
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ExampleApp()
|
||||||
|
app.run()
|
||||||
25
docs/examples/guide/dom3.py
Normal file
25
docs/examples/guide/dom3.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Container, Horizontal
|
||||||
|
from textual.widgets import Button, Footer, Header, Static
|
||||||
|
|
||||||
|
QUESTION = "Do you want to learn about Textual CSS?"
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Footer()
|
||||||
|
yield Container(
|
||||||
|
Static(QUESTION, classes="question"),
|
||||||
|
Horizontal(
|
||||||
|
Button("Yes", variant="success"),
|
||||||
|
Button("No", variant="error"),
|
||||||
|
classes="buttons",
|
||||||
|
),
|
||||||
|
id="dialog",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ExampleApp()
|
||||||
|
app.run()
|
||||||
30
docs/examples/guide/dom4.css
Normal file
30
docs/examples/guide/dom4.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
/* The top level dialog (a Container) */
|
||||||
|
#dialog {
|
||||||
|
margin: 4 8;
|
||||||
|
background: $panel;
|
||||||
|
color: $text;
|
||||||
|
border: tall $background;
|
||||||
|
padding: 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The button class */
|
||||||
|
Button {
|
||||||
|
width: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matches the question text */
|
||||||
|
.question {
|
||||||
|
text-style: bold;
|
||||||
|
height: 100%;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matches the button container */
|
||||||
|
.buttons {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
dock: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
27
docs/examples/guide/dom4.py
Normal file
27
docs/examples/guide/dom4.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Container, Horizontal
|
||||||
|
from textual.widgets import Header, Footer, Static, Button
|
||||||
|
|
||||||
|
QUESTION = "Do you want to learn about Textual CSS?"
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleApp(App):
|
||||||
|
CSS_PATH = "dom4.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Footer()
|
||||||
|
yield Container(
|
||||||
|
Static(QUESTION, classes="question"),
|
||||||
|
Horizontal(
|
||||||
|
Button("Yes", variant="success"),
|
||||||
|
Button("No", variant="error"),
|
||||||
|
classes="buttons",
|
||||||
|
),
|
||||||
|
id="dialog",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ExampleApp()
|
||||||
|
app.run()
|
||||||
7
docs/examples/guide/input/binding01.css
Normal file
7
docs/examples/guide/input/binding01.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bar {
|
||||||
|
height: 5;
|
||||||
|
content-align: center middle;
|
||||||
|
text-style: bold;
|
||||||
|
margin: 1 2;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
31
docs/examples/guide/input/binding01.py
Normal file
31
docs/examples/guide/input/binding01.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.color import Color
|
||||||
|
from textual.widgets import Footer, Static
|
||||||
|
|
||||||
|
|
||||||
|
class Bar(Static):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BindingApp(App):
|
||||||
|
|
||||||
|
CSS_PATH = "binding01.css"
|
||||||
|
BINDINGS = [
|
||||||
|
("r", "add_bar('red')", "Add Red"),
|
||||||
|
("g", "add_bar('green')", "Add Green"),
|
||||||
|
("b", "add_bar('blue')", "Add Blue"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def action_add_bar(self, color: str) -> None:
|
||||||
|
bar = Bar(color)
|
||||||
|
bar.styles.background = Color.parse(color).with_alpha(0.5)
|
||||||
|
self.mount(bar)
|
||||||
|
self.call_later(self.screen.scroll_end, animate=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = BindingApp()
|
||||||
|
app.run()
|
||||||
18
docs/examples/guide/input/key01.py
Normal file
18
docs/examples/guide/input/key01.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import TextLog
|
||||||
|
from textual import events
|
||||||
|
|
||||||
|
|
||||||
|
class InputApp(App):
|
||||||
|
"""App to display key events."""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield TextLog()
|
||||||
|
|
||||||
|
def on_key(self, event: events.Key) -> None:
|
||||||
|
self.query_one(TextLog).write(event)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = InputApp()
|
||||||
|
app.run()
|
||||||
21
docs/examples/guide/input/key02.py
Normal file
21
docs/examples/guide/input/key02.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import TextLog
|
||||||
|
from textual import events
|
||||||
|
|
||||||
|
|
||||||
|
class InputApp(App):
|
||||||
|
"""App to display key events."""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield TextLog()
|
||||||
|
|
||||||
|
def on_key(self, event: events.Key) -> None:
|
||||||
|
self.query_one(TextLog).write(event)
|
||||||
|
|
||||||
|
def key_space(self) -> None:
|
||||||
|
self.bell()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = InputApp()
|
||||||
|
app.run()
|
||||||
17
docs/examples/guide/input/key03.css
Normal file
17
docs/examples/guide/input/key03.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 2 2;
|
||||||
|
grid-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyLogger {
|
||||||
|
border: blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyLogger:hover {
|
||||||
|
border: wide $secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyLogger:focus {
|
||||||
|
border: wide $accent;
|
||||||
|
}
|
||||||
25
docs/examples/guide/input/key03.py
Normal file
25
docs/examples/guide/input/key03.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import TextLog
|
||||||
|
from textual import events
|
||||||
|
|
||||||
|
|
||||||
|
class KeyLogger(TextLog):
|
||||||
|
def on_key(self, event: events.Key) -> None:
|
||||||
|
self.write(event)
|
||||||
|
|
||||||
|
|
||||||
|
class InputApp(App):
|
||||||
|
"""App to display key events."""
|
||||||
|
|
||||||
|
CSS_PATH = "key03.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield KeyLogger()
|
||||||
|
yield KeyLogger()
|
||||||
|
yield KeyLogger()
|
||||||
|
yield KeyLogger()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = InputApp()
|
||||||
|
app.run()
|
||||||
24
docs/examples/guide/input/mouse01.css
Normal file
24
docs/examples/guide/input/mouse01.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Screen {
|
||||||
|
layers: log ball;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextLog {
|
||||||
|
layer: log;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayArea {
|
||||||
|
background: transparent;
|
||||||
|
layer: ball;
|
||||||
|
|
||||||
|
}
|
||||||
|
Ball {
|
||||||
|
layer: ball;
|
||||||
|
width: auto;
|
||||||
|
height: 1;
|
||||||
|
background: $secondary;
|
||||||
|
border: tall $secondary;
|
||||||
|
color: $background;
|
||||||
|
box-sizing: content-box;
|
||||||
|
text-style: bold;
|
||||||
|
padding: 0 4;
|
||||||
|
}
|
||||||
30
docs/examples/guide/input/mouse01.py
Normal file
30
docs/examples/guide/input/mouse01.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from textual import events
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Container
|
||||||
|
from textual.widgets import Static, TextLog
|
||||||
|
|
||||||
|
|
||||||
|
class PlayArea(Container):
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.capture_mouse()
|
||||||
|
|
||||||
|
def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||||
|
self.screen.query_one(TextLog).write(event)
|
||||||
|
self.query_one(Ball).offset = event.offset - (8, 2)
|
||||||
|
|
||||||
|
|
||||||
|
class Ball(Static):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MouseApp(App):
|
||||||
|
CSS_PATH = "mouse01.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield TextLog()
|
||||||
|
yield PlayArea(Ball("Textual"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = MouseApp()
|
||||||
|
app.run()
|
||||||
50
docs/examples/guide/layout/combining_layouts.css
Normal file
50
docs/examples/guide/layout/combining_layouts.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#app-grid {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 2; /* two columns */
|
||||||
|
grid-columns: 1fr;
|
||||||
|
grid-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#left-pane > Static {
|
||||||
|
background: $boost;
|
||||||
|
color: auto;
|
||||||
|
margin-bottom: 1;
|
||||||
|
padding: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#left-pane {
|
||||||
|
row-span: 2;
|
||||||
|
background: $panel;
|
||||||
|
border: dodgerblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-right {
|
||||||
|
background: $panel;
|
||||||
|
border: mediumvioletred;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-right > Static {
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 1;
|
||||||
|
background: $boost;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottom-right {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
grid-columns: 1fr;
|
||||||
|
grid-rows: 1fr;
|
||||||
|
grid-gutter: 1;
|
||||||
|
background: $panel;
|
||||||
|
border: greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottom-right-final {
|
||||||
|
column-span: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottom-right > Static {
|
||||||
|
height: 100%;
|
||||||
|
background: $boost;
|
||||||
|
}
|
||||||
37
docs/examples/guide/layout/combining_layouts.py
Normal file
37
docs/examples/guide/layout/combining_layouts.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from textual.containers import Container, Horizontal, Vertical
|
||||||
|
from textual.app import ComposeResult, App
|
||||||
|
from textual.widgets import Static, Header
|
||||||
|
|
||||||
|
|
||||||
|
class CombiningLayoutsExample(App):
|
||||||
|
CSS_PATH = "combining_layouts.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Container(
|
||||||
|
Vertical(
|
||||||
|
*[Static(f"Vertical layout, child {number}") for number in range(15)],
|
||||||
|
id="left-pane",
|
||||||
|
),
|
||||||
|
Horizontal(
|
||||||
|
Static("Horizontally"),
|
||||||
|
Static("Positioned"),
|
||||||
|
Static("Children"),
|
||||||
|
Static("Here"),
|
||||||
|
id="top-right",
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
Static("This"),
|
||||||
|
Static("panel"),
|
||||||
|
Static("is"),
|
||||||
|
Static("using"),
|
||||||
|
Static("grid layout!", id="bottom-right-final"),
|
||||||
|
id="bottom-right",
|
||||||
|
),
|
||||||
|
id="app-grid",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = CombiningLayoutsExample()
|
||||||
|
app.run()
|
||||||
7
docs/examples/guide/layout/dock_layout1_sidebar.css
Normal file
7
docs/examples/guide/layout/dock_layout1_sidebar.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#sidebar {
|
||||||
|
dock: left;
|
||||||
|
width: 15;
|
||||||
|
height: 100%;
|
||||||
|
color: #0f2b41;
|
||||||
|
background: dodgerblue;
|
||||||
|
}
|
||||||
22
docs/examples/guide/layout/dock_layout1_sidebar.py
Normal file
22
docs/examples/guide/layout/dock_layout1_sidebar.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
TEXT = """\
|
||||||
|
Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.
|
||||||
|
|
||||||
|
Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DockLayoutExample(App):
|
||||||
|
CSS_PATH = "dock_layout1_sidebar.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("Sidebar", id="sidebar")
|
||||||
|
yield Static(TEXT * 10, id="body")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = DockLayoutExample()
|
||||||
|
app.run()
|
||||||
14
docs/examples/guide/layout/dock_layout2_sidebar.css
Normal file
14
docs/examples/guide/layout/dock_layout2_sidebar.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#another-sidebar {
|
||||||
|
dock: left;
|
||||||
|
width: 30;
|
||||||
|
height: 100%;
|
||||||
|
background: deeppink;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
dock: left;
|
||||||
|
width: 15;
|
||||||
|
height: 100%;
|
||||||
|
color: #0f2b41;
|
||||||
|
background: dodgerblue;
|
||||||
|
}
|
||||||
23
docs/examples/guide/layout/dock_layout2_sidebar.py
Normal file
23
docs/examples/guide/layout/dock_layout2_sidebar.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
TEXT = """\
|
||||||
|
Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.
|
||||||
|
|
||||||
|
Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DockLayoutExample(App):
|
||||||
|
CSS_PATH = "dock_layout2_sidebar.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("Sidebar2", id="another-sidebar")
|
||||||
|
yield Static("Sidebar1", id="sidebar")
|
||||||
|
yield Static(TEXT * 10, id="body")
|
||||||
|
|
||||||
|
|
||||||
|
app = DockLayoutExample()
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#sidebar {
|
||||||
|
dock: left;
|
||||||
|
width: 15;
|
||||||
|
height: 100%;
|
||||||
|
color: #0f2b41;
|
||||||
|
background: dodgerblue;
|
||||||
|
}
|
||||||
23
docs/examples/guide/layout/dock_layout3_sidebar_header.py
Normal file
23
docs/examples/guide/layout/dock_layout3_sidebar_header.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static, Header
|
||||||
|
|
||||||
|
TEXT = """\
|
||||||
|
Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.
|
||||||
|
|
||||||
|
Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DockLayoutExample(App):
|
||||||
|
CSS_PATH = "dock_layout3_sidebar_header.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header(id="header")
|
||||||
|
yield Static("Sidebar1", id="sidebar")
|
||||||
|
yield Static(TEXT * 10, id="body")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = DockLayoutExample()
|
||||||
|
app.run()
|
||||||
9
docs/examples/guide/layout/grid_layout1.css
Normal file
9
docs/examples/guide/layout/grid_layout1.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
19
docs/examples/guide/layout/grid_layout1.py
Normal file
19
docs/examples/guide/layout/grid_layout1.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class GridLayoutExample(App):
|
||||||
|
CSS_PATH = "grid_layout1.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two", classes="box")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
yield Static("Four", classes="box")
|
||||||
|
yield Static("Five", classes="box")
|
||||||
|
yield Static("Six", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = GridLayoutExample()
|
||||||
|
app.run()
|
||||||
9
docs/examples/guide/layout/grid_layout2.css
Normal file
9
docs/examples/guide/layout/grid_layout2.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
20
docs/examples/guide/layout/grid_layout2.py
Normal file
20
docs/examples/guide/layout/grid_layout2.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class GridLayoutExample(App):
|
||||||
|
CSS_PATH = "grid_layout2.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two", classes="box")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
yield Static("Four", classes="box")
|
||||||
|
yield Static("Five", classes="box")
|
||||||
|
yield Static("Six", classes="box")
|
||||||
|
yield Static("Seven", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = GridLayoutExample()
|
||||||
|
app.run()
|
||||||
10
docs/examples/guide/layout/grid_layout3_row_col_adjust.css
Normal file
10
docs/examples/guide/layout/grid_layout3_row_col_adjust.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
grid-columns: 2fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
19
docs/examples/guide/layout/grid_layout3_row_col_adjust.py
Normal file
19
docs/examples/guide/layout/grid_layout3_row_col_adjust.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class GridLayoutExample(App):
|
||||||
|
CSS_PATH = "grid_layout3_row_col_adjust.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two", classes="box")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
yield Static("Four", classes="box")
|
||||||
|
yield Static("Five", classes="box")
|
||||||
|
yield Static("Six", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = GridLayoutExample()
|
||||||
|
app.run()
|
||||||
11
docs/examples/guide/layout/grid_layout4_row_col_adjust.css
Normal file
11
docs/examples/guide/layout/grid_layout4_row_col_adjust.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
grid-columns: 2fr 1fr 1fr;
|
||||||
|
grid-rows: 25% 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
19
docs/examples/guide/layout/grid_layout4_row_col_adjust.py
Normal file
19
docs/examples/guide/layout/grid_layout4_row_col_adjust.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class GridLayoutExample(App):
|
||||||
|
CSS_PATH = "grid_layout4_row_col_adjust.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two", classes="box")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
yield Static("Four", classes="box")
|
||||||
|
yield Static("Five", classes="box")
|
||||||
|
yield Static("Six", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = GridLayoutExample()
|
||||||
|
app.run()
|
||||||
14
docs/examples/guide/layout/grid_layout5_col_span.css
Normal file
14
docs/examples/guide/layout/grid_layout5_col_span.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#two {
|
||||||
|
column-span: 2;
|
||||||
|
tint: magenta 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
19
docs/examples/guide/layout/grid_layout5_col_span.py
Normal file
19
docs/examples/guide/layout/grid_layout5_col_span.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class GridLayoutExample(App):
|
||||||
|
CSS_PATH = "grid_layout5_col_span.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two [b](column-span: 2)", classes="box", id="two")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
yield Static("Four", classes="box")
|
||||||
|
yield Static("Five", classes="box")
|
||||||
|
yield Static("Six", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = GridLayoutExample()
|
||||||
|
app.run()
|
||||||
15
docs/examples/guide/layout/grid_layout6_row_span.css
Normal file
15
docs/examples/guide/layout/grid_layout6_row_span.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#two {
|
||||||
|
column-span: 2;
|
||||||
|
row-span: 2;
|
||||||
|
tint: magenta 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
19
docs/examples/guide/layout/grid_layout6_row_span.py
Normal file
19
docs/examples/guide/layout/grid_layout6_row_span.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class GridLayoutExample(App):
|
||||||
|
CSS_PATH = "grid_layout6_row_span.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two [b](column-span: 2 and row-span: 2)", classes="box", id="two")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
yield Static("Four", classes="box")
|
||||||
|
yield Static("Five", classes="box")
|
||||||
|
yield Static("Six", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
app = GridLayoutExample()
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
11
docs/examples/guide/layout/grid_layout7_gutter.css
Normal file
11
docs/examples/guide/layout/grid_layout7_gutter.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Screen {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
grid-gutter: 1;
|
||||||
|
background: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
background: darkmagenta;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
19
docs/examples/guide/layout/grid_layout7_gutter.py
Normal file
19
docs/examples/guide/layout/grid_layout7_gutter.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class GridLayoutExample(App):
|
||||||
|
CSS_PATH = "grid_layout7_gutter.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two", classes="box")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
yield Static("Four", classes="box")
|
||||||
|
yield Static("Five", classes="box")
|
||||||
|
yield Static("Six", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = GridLayoutExample()
|
||||||
|
app.run()
|
||||||
9
docs/examples/guide/layout/horizontal_layout.css
Normal file
9
docs/examples/guide/layout/horizontal_layout.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Screen {
|
||||||
|
layout: horizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
width: 1fr;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
16
docs/examples/guide/layout/horizontal_layout.py
Normal file
16
docs/examples/guide/layout/horizontal_layout.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class HorizontalLayoutExample(App):
|
||||||
|
CSS_PATH = "horizontal_layout.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two", classes="box")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = HorizontalLayoutExample()
|
||||||
|
app.run()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Screen {
|
||||||
|
layout: horizontal;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
}
|
||||||
16
docs/examples/guide/layout/horizontal_layout_overflow.py
Normal file
16
docs/examples/guide/layout/horizontal_layout_overflow.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class HorizontalLayoutExample(App):
|
||||||
|
CSS_PATH = "horizontal_layout_overflow.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("One", classes="box")
|
||||||
|
yield Static("Two", classes="box")
|
||||||
|
yield Static("Three", classes="box")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = HorizontalLayoutExample()
|
||||||
|
app.run()
|
||||||
22
docs/examples/guide/layout/layers.css
Normal file
22
docs/examples/guide/layout/layers.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Screen {
|
||||||
|
align: center middle;
|
||||||
|
layers: below above;
|
||||||
|
}
|
||||||
|
|
||||||
|
Static {
|
||||||
|
width: 28;
|
||||||
|
height: 8;
|
||||||
|
color: auto;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#box1 {
|
||||||
|
background: darkcyan;
|
||||||
|
layer: above;
|
||||||
|
}
|
||||||
|
|
||||||
|
#box2 {
|
||||||
|
layer: below;
|
||||||
|
background: orange;
|
||||||
|
offset: 12 6;
|
||||||
|
}
|
||||||
15
docs/examples/guide/layout/layers.py
Normal file
15
docs/examples/guide/layout/layers.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class LayersExample(App):
|
||||||
|
CSS_PATH = "layers.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Static("box1 (layer = above)", id="box1")
|
||||||
|
yield Static("box2 (layer = below)", id="box2")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = LayersExample()
|
||||||
|
app.run()
|
||||||
10
docs/examples/guide/layout/utility_containers.css
Normal file
10
docs/examples/guide/layout/utility_containers.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Static {
|
||||||
|
content-align: center middle;
|
||||||
|
background: crimson;
|
||||||
|
border: solid darkred;
|
||||||
|
height: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
width: 1fr;
|
||||||
|
}
|
||||||
26
docs/examples/guide/layout/utility_containers.py
Normal file
26
docs/examples/guide/layout/utility_containers.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Horizontal, Vertical
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class UtilityContainersExample(App):
|
||||||
|
CSS_PATH = "utility_containers.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Horizontal(
|
||||||
|
Vertical(
|
||||||
|
Static("One"),
|
||||||
|
Static("Two"),
|
||||||
|
classes="column",
|
||||||
|
),
|
||||||
|
Vertical(
|
||||||
|
Static("Three"),
|
||||||
|
Static("Four"),
|
||||||
|
classes="column",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = UtilityContainersExample()
|
||||||
|
app.run()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user