Merge pull request #979 from Textualize/css

CSS
This commit is contained in:
Will McGugan
2022-10-22 18:52:33 +01:00
committed by GitHub
588 changed files with 46566 additions and 5431 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1 @@
# These are supported funding model platforms
ko_fi: textualize

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -1,375 +1,121 @@
# Textual
![screenshot](./imgs/textual.png)
![Textual splash image](./imgs/textual.png)
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
[![Join the chat at https://gitter.im/textual-ui/community](https://badges.gitter.im/textual-ui/community.svg)](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.
![Textual demo](./imgs/demo.svg)
## 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.
![calculator screenshot](./imgs/calculator.svg)
</details>
<details>
<summary> 📷 Stopwatch </summary>
<hr>
This is the Stopwatch example from the tutorial.
### Light theme
![stopwatch light screenshot](./imgs/stopwatch_light.svg)
### Dark theme
![stopwatch dark screenshot](./imgs/stopwatch_dark.svg)
</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:
![widgets](./imgs/widgets.png)
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:
![widgets](./imgs/custom.gif)
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
[![Textual update 1](https://yt-embed.herokuapp.com/embed?v=zNW7U36GHlU&img=0)](http://www.youtube.com/watch?v=zNW7U36GHlU)
### Update 2 - Keyboard toggle
[![Textual update 2](https://yt-embed.herokuapp.com/embed?v=bTYeFOVNXDI&img=0)](http://www.youtube.com/watch?v=bTYeFOVNXDI)
### Update 3 - New scrollbars and smooth scrolling
[![Textual update 3](https://yt-embed.herokuapp.com/embed?v=4LVl3ClrXIs&img=0)](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.
[![Textual update 4](https://yt-embed.herokuapp.com/embed?v=k2VwOp1YbSk&img=0)](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!
[![Textual update 5](https://yt-embed.herokuapp.com/embed?v=XxRnfx2WYRw&img=0)](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.
[![Textual update 6](https://yt-embed.herokuapp.com/embed?v=jddccDuVd3E&img=0)](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.
[![Textual update 7](https://yt-embed.herokuapp.com/embed?v=Zh9CEvu73jc&img=0)](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
[![Textual update 8](https://yt-embed.herokuapp.com/embed?v=J-dzzD6NQJ4&img=0)](http://www.youtube.com/watch?v=J-dzzD6NQJ4)
</details>

18
docs.md Normal file
View 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
View File

@@ -0,0 +1 @@
textual.textualize.io

View 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
View 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
View 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

View 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

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
# Events
A reference to Textual [events](../guide/events.md).

17
docs/events/key.md Normal file
View 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
View 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
View 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
View 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

View 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
View 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
View 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

View 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

View 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

View 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
View 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
View 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
View 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

View 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

View 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
View 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

View File

@@ -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()

View File

@@ -1,9 +0,0 @@
from textual.app import App
class Quiter(App):
async def on_load(self):
await self.bind("q", "quit")
Quiter.run()

View 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()

View 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)

View 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%;
}

View 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)

View 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)

View File

@@ -0,0 +1,5 @@
from textual.app import App
class MyApp(App):
pass

View File

@@ -0,0 +1,10 @@
from textual.app import App
class MyApp(App):
pass
if __name__ == "__main__":
app = MyApp()
app.run()

View 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()

View 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()

View 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()

View 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%;
}

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View File

@@ -0,0 +1,11 @@
Screen {
layout: grid;
grid-size: 1;
grid-gutter: 2 4;
grid-rows: 1fr;
}
ColorSwitcher {
height: 100%;
margin: 2 4;
}

View 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()

View 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()

View 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()

View File

@@ -0,0 +1,10 @@
from textual.app import App
class ExampleApp(App):
pass
if __name__ == "__main__":
app = ExampleApp()
app.run()

View 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()

View 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()

View 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;
}

View 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()

View File

@@ -0,0 +1,7 @@
Bar {
height: 5;
content-align: center middle;
text-style: bold;
margin: 1 2;
color: $text;
}

View 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()

View 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()

View 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()

View 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;
}

View 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()

View 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;
}

View 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()

View 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;
}

View 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()

View File

@@ -0,0 +1,7 @@
#sidebar {
dock: left;
width: 15;
height: 100%;
color: #0f2b41;
background: dodgerblue;
}

View 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()

View 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;
}

View 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()

View File

@@ -0,0 +1,7 @@
#sidebar {
dock: left;
width: 15;
height: 100%;
color: #0f2b41;
background: dodgerblue;
}

View 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()

View File

@@ -0,0 +1,9 @@
Screen {
layout: grid;
grid-size: 3 2;
}
.box {
height: 100%;
border: solid green;
}

View 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()

View File

@@ -0,0 +1,9 @@
Screen {
layout: grid;
grid-size: 3;
}
.box {
height: 100%;
border: solid green;
}

View 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()

View File

@@ -0,0 +1,10 @@
Screen {
layout: grid;
grid-size: 3;
grid-columns: 2fr 1fr 1fr;
}
.box {
height: 100%;
border: solid green;
}

View 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()

View 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;
}

View 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()

View File

@@ -0,0 +1,14 @@
Screen {
layout: grid;
grid-size: 3;
}
#two {
column-span: 2;
tint: magenta 40%;
}
.box {
height: 100%;
border: solid green;
}

View 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()

View 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;
}

View 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()

View File

@@ -0,0 +1,11 @@
Screen {
layout: grid;
grid-size: 3;
grid-gutter: 1;
background: lightgreen;
}
.box {
background: darkmagenta;
height: 100%;
}

View 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()

View File

@@ -0,0 +1,9 @@
Screen {
layout: horizontal;
}
.box {
height: 100%;
width: 1fr;
border: solid green;
}

View 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()

View File

@@ -0,0 +1,9 @@
Screen {
layout: horizontal;
overflow-x: auto;
}
.box {
height: 100%;
border: solid green;
}

View 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()

View 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;
}

View 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()

View File

@@ -0,0 +1,10 @@
Static {
content-align: center middle;
background: crimson;
border: solid darkred;
height: 1fr;
}
.column {
width: 1fr;
}

View 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