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
|
||||
|
||||
ko_fi: textualize
|
||||
|
||||
18
.github/workflows/pythonpackage.yml
vendored
18
.github/workflows/pythonpackage.yml
vendored
@@ -7,8 +7,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
python-version: ["3.7", "3.8", "3.9"]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
version: 1.1.6
|
||||
virtualenvs-in-project: true
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
run: poetry install --extras "dev"
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Format check with black
|
||||
run: |
|
||||
@@ -39,11 +39,9 @@ jobs:
|
||||
run: |
|
||||
source $VENV
|
||||
pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v1.0.10
|
||||
- name: Upload snapshot report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
name: rich
|
||||
flags: unittests
|
||||
env_vars: OS,PYTHON
|
||||
name: snapshot-report-textual
|
||||
path: tests/snapshot_tests/output/snapshot_report.html
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
.pytype
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
mypy_report
|
||||
docs/build
|
||||
docs/source/_build
|
||||
@@ -112,3 +113,6 @@ venv.bak/
|
||||
|
||||
# mypy
|
||||
.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
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
args: ['--unsafe']
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.8b0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- 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/)
|
||||
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
|
||||
|
||||
### Changed
|
||||
|
||||
12
Makefile
12
Makefile
@@ -1,8 +1,18 @@
|
||||
test:
|
||||
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:
|
||||
mypy src/textual
|
||||
format:
|
||||
black src
|
||||
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 is a TUI (Text User Interface) framework for Python inspired by modern web development. Currently a **Work in Progress**.
|
||||
|
||||
> **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.
|
||||
Textual is a Python framework for creating interactive applications that run in your terminal.
|
||||
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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/).
|
||||
Install Textual via pip:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
Until I've written the documentation, the [examples](https://github.com/willmcgugan/textual/tree/main/examples/) may be the best way to learn Textual.
|
||||
|
||||
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
|
||||
The Textual repository comes with a number of examples you can experiment with or use as a template for your own projects.
|
||||
|
||||
|
||||
class Beeper(App):
|
||||
def on_key(self):
|
||||
self.console.bell()
|
||||
<details>
|
||||
<summary> 🎬 Code browser </summary>
|
||||
<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.
|
||||
|
||||
Let's look at a _slightly_ more interesting example:
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||
https://user-images.githubusercontent.com/554369/196157100-352852a6-2b09-4dc8-a888-55b53570aff9.mov
|
||||
|
||||
|
||||
class ColorChanger(App):
|
||||
def on_key(self, event):
|
||||
if event.key.isdigit():
|
||||
self.background = f"on color({event.key})"
|
||||
</details>
|
||||
|
||||
|
||||
ColorChanger.run(log="textual.log")
|
||||
<details>
|
||||
<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
|
||||
|
||||
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)
|
||||
|
||||
</details>
|
||||
|
||||
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