Merge pull request #540 from Textualize/docs-structure

Docs Refresh
This commit is contained in:
Will McGugan
2022-05-27 12:01:25 +01:00
committed by GitHub
51 changed files with 587 additions and 173 deletions

5
custom_theme/main.html Normal file
View File

@@ -0,0 +1,5 @@
{% 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" />
{% endblock %}

1
docs/actions.md Normal file
View File

@@ -0,0 +1 @@
# Actions

15
docs/events/mount.md Normal file
View File

@@ -0,0 +1,15 @@
# Mount
The `Mount` event is sent to a widget and Application when it is first mounted.
- [ ] Bubbles
## Parameters
`sender`
: The sender of the widget
## Code
::: textual.events.Mount

23
docs/events/resize.md Normal file
View File

@@ -0,0 +1,23 @@
# Resize
The `Resize` event is sent to a widget when its size changes and when it is first made visible.
- [x] Bubbles
## Parameters
`event.size`
: The new size of the Widget.
`event.virtual_size`
: The virtual size (scrollable area) of the Widget.
`event.container_size`
: The size of the widget's container.
## Code
::: textual.events.Resize

View File

@@ -0,0 +1,22 @@
from datetime import datetime
from textual.app import App
from textual.widget import Widget
class Clock(Widget):
def on_mount(self):
self.styles.content_align = ("center", "middle")
self.set_interval(1, self.refresh)
def render(self):
return datetime.now().strftime("%c")
class ClockApp(App):
def compose(self):
yield Clock()
app = ClockApp()
app.run()

View File

@@ -0,0 +1,22 @@
from datetime import datetime
from textual.app import App
from textual.widget import Widget
class Clock(Widget):
def on_mount(self):
self.styles.content_align = ("center", "middle")
self.set_interval(1, self.refresh)
def render(self):
return datetime.now().strftime("%c")
class ClockApp(App):
def on_mount(self):
self.mount(Clock())
app = ClockApp()
app.run()

View File

@@ -0,0 +1,9 @@
from textual.app import App
class ExampleApp(App):
pass
app = ExampleApp()
app.run()

View File

@@ -0,0 +1,29 @@
from textual.app import App
class ExampleApp(App):
COLORS = [
"white",
"maroon",
"red",
"purple",
"fuchsia",
"olive",
"yellow",
"navy",
"teal",
"aqua",
]
def on_mount(self):
self.styles.background = "darkblue"
def on_key(self, event):
if event.key.isdigit():
self.styles.background = self.COLORS[int(event.key)]
self.bell()
app = ExampleApp()
app.run()

View File

@@ -0,0 +1,17 @@
from textual.app import App
from textual.widget import Widget
class WidthApp(App):
CSS = """
Widget {
background: blue 50%;
width: 50%;
}
"""
def compose(self):
yield Widget()
app = WidthApp()

1
docs/guide/guide.md Normal file
View File

@@ -0,0 +1 @@
# Guide

View File

@@ -1,30 +1,11 @@
# Welcome to Textual documentation # Welcome
Textual is framework for rapidly creating _text user interfaces_ (TUIs from here on) with Python. Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation, built with ❤️ by [Textualize.io](https://www.textualize.io)
A TUI is an application that lives within a terminal, which can have mouse and keyboard support and user interface elements like windows and panels, but is rendered purely with text. They have a number of advantages over GUI applications: they can be launched from the command line, and return to the command line, and they work over ssh. ## Getting started
## Foo Textual is a Python framework which you can install via Pypi.
Creating a TUI can be challenging. It may be easier to create a GUI or web application than it is to build a TUI with traditional techniques. Often projects that could use one or the other never manage to ship either. ```bash
pip install textual
Textual seeks to lower the difficulty level of building a TUI by borrowing developments from the web world and to a lesser extent desktop applications. The goal is for it to be as easy to develop a TUI for your project as it would be to add a command line interface.XX ```
=== "simple.py"
```python
--8<-- "docs/examples/simple.py"
```
=== "simple.css"
```scss
--8<-- "docs/examples/simple.css"
```
=== "Result"
```{.textual path="docs/examples/simple.py" columns="80" lines="24"}
```
Textual also offers a number of enhancements over traditional TUI applications by taking advantage of improvements to terminal software and the hardware it runs on. Terminals are a far cry from their roots in ancient hardware and dial-up modems, yet much of the software that runs on them hasn't kept pace.

147
docs/introduction.md Normal file
View File

@@ -0,0 +1,147 @@
# Introduction
Welcome to the Textual Introduction!
This is a very gentle introduction to creating Textual applications. By the end of this document you should have an understanding of the basic concepts involved in using the Textual framework.
## Pre-requisites
- Python 3.7 or later. If you have a choice, pick the most recent version.
- Installed `textual` from Pypi.
- Basic Python skills.
## A Simple App
Let's looks at the simplest possible Textual app. It doesn't do much, but will demonstrate the basic steps you will need to create any application.
If you would like to follow along and run the examples, navigate to the `docs/examples/introduction` directory from the command prompt. We will be looking at `intro01.py`, which you can see here:
```python title="intro01.py"
--8<-- "docs/examples/introduction/intro01.py"
```
Enter the following command to run the application:
```bash
python intro01.py
```
The command prompt should disappear and you will see a blank screen. It will look something like the following:
```{.textual path="docs/examples/introduction/intro01.py"}
```
Hit ++ctrl+c++ to exit and return to the command prompt.
The first step in all Textual applications is to import the `App` class from `textual.app` and extend it:
```python hl_lines="1 2 3 4 5" title="intro01.py"
--8<-- "docs/examples/introduction/intro01.py"
```
There will be a single App class in any Textual application. The App class is responsible for loading data, setting up the screen, managing events etc. In a real app most of the core logic of your application will be contained within methods on the this class.
The last two lines create an instance of the application and calls `run()`:
```python hl_lines="8 9" title="intro01.py"
--8<-- "docs/examples/introduction/intro01.py"
```
The `run` method will put your terminal in to "application mode" which disables the prompt and allows Textual to take over input and output. When you press ++ctrl+c++ the application will exit application mode and re-enable the command prompt.
## Handling Events
In the previously example our app did next to nothing. Most applications will contain event handler methods, which are called in response to user actions such as key presses and mouse movements in addition to other changes your app needs to know about such as terminal resize, scrolling, timers, etc.
!!! note
Although `intro01.py` did not explicitly define any event handlers, Textual still had to respond to events to catch ++ctrl+c++, otherwise you wouldn't be able to exit the app.
In our next example, we are going to handle two events; `Mount` and `Key`. The `Mount` event is sent when the app is first run, and a `Key` event is sent when the user presses a key on the keyboard. Try running `intro02.py` in the `docs/examples/introduction`:
```python title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
```
When you run this app you should see a blue screen in your terminal, like the following:
```{.textual path="docs/examples/introduction/intro02.py"}
```
If you hit any of the number keys ++0++-++9++, the background will change color and you should hear a beep. As before, pressing ++ctrl+c++ will exit the app and return you to your prompt.
!!! note
The "beep" is your terminal's *bell*. Some terminals may be configured to play different noises or a visual indication of a bell rather than a noise.
There are two event handlers in this app. Event handlers start with the text `on_` followed by the name of the event in lower case. Hence `on_mount` is called for the `Mount` event, and `on_key` is called for the `Key` event.
The first event handler to run is `on_mount`:
```python hl_lines="19 20" title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
```
This `on_mount` method sets the `background` attribute of `self.styles` to `"darkblue"` which updates the background color. There are a lot of other properties on the Styles object, which define how your app looks. We will explore what you can do with this object later.
!!! note
You may have noticed there was no function call to repaint the screen in this examples. Textual will detect when a refresh is required, and do it automatically.
The second event handler will receive `Key` events whenever you press a key on the keyboard:
```python hl_lines="22 23 24 25" title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
```
This method has an `event` positional argument which contains information regarding the key that was pressed. The body of the method sets the background to a corresponding color int the `COLORS` list when you press one of the digit keys. It also calls `bell()` which is a method on App that plays your terminal's bell.
!!! note
Every event has a corresponding `Event` object, but Textual knows to only call the event handler with the event object if you have it in the argument list. It does this by inspecting the handler method prior to calling it.
## Widgets
Most Textual applications will also make use of one or more `Widget` classes. A Widget is a self contained component responsible for defining how a given part of the screen should look. Widgets respond to events in much the same way as the App does.
Let's look at an app with a simple Widget to show the current time and date. Here is the code for `"clock01.py"` which is in the same directory as the previous examples:
```python title="clock01.py"
--8<-- "docs/examples/introduction/clock01.py"
```
Here's what you will see if you run this code:
```{.textual path="docs/examples/introduction/clock01.py"}
```
This script imports App as before, but also the `Widget` class from `textual.widget`, which is the base class for all Widgets. To create a Clock widget we extend from the Widget base class:
```python title="clock01.py" hl_lines="7 8 9 10 11 12 13"
--8<-- "docs/examples/introduction/clock01.py"
```
Widgets support many of the same events as the Application itself, and can be thought of as mini-applications in their own right. The Clock widget also responds to a Mount event which is the first event received when a widget is _mounted_ (added to the App). The code in `Clock.on_mount` sets `styles.content_align` to tuple of `("center", "middle")` which tells Textual to display the Widget's content aligned to the horizontal center, and in the middle vertically. If you resize the terminal, you should find the time remains in the center.
The second line in `on_mount` calls `self.set_interval` which tells Textual to invoke the `self.refresh` function once a second to refresh the Clock widget.
When Textual refreshes a widget it calls it's `render` method:
```python title="clock01.py" hl_lines="12 13"
--8<-- "docs/examples/introduction/clock01.py"
```
The Clocks `render` method uses the datetime module to format the current date and time. It returns a string, but can also return a _Rich renderable_. Don't worry if you aren't familiar with [Rich](https://github.com/Textualize/rich), we will cover that later.
Before a Widget can be displayed, it must first be mounted on the app. This is typically done within the applications Mount handler, so that an application's widgets are added when the application first starts:
```python title="clock01.py" hl_lines="17 18"
--8<-- "docs/examples/introduction/clock01.py"
```
In the case of the clock application, we call `mount` with an instance of the `Clock` widget.
That's all there is to this Clock example. It will display the current time until you hit ++ctrl+c++

1
docs/reference/widget.md Normal file
View File

@@ -0,0 +1 @@
::: textual.widget.Widget

37
docs/styles/width.md Normal file
View File

@@ -0,0 +1,37 @@
# Width
The `width` property sets a widget's width. By default, it sets the width of the content area, but if `box-sizing` is set to `border-box` it sets the width of the border area.
## Example
=== "width.py"
```python
--8<-- "docs/examples/styles/width.py"
```
=== "Output"
```{.textual path="docs/examples/styles/width.py"}
```
## CSS
```sass
/* Explicit cell width */
width: 10;
/* Percentage width */
width: 50%;
/* Automatic width */
width: auto
```
## Python
```python
self.styles.width = 10
self.styles.width = "50%
self.styles.width = "auto"
```

View File

@@ -53,12 +53,12 @@ lorem = Text.from_markup(
class TweetHeader(Widget): class TweetHeader(Widget):
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return Text("Lorem Impsum", justify="center") return Text("Lorem Impsum", justify="center")
class TweetBody(Widget): class TweetBody(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return lorem return lorem
@@ -67,22 +67,22 @@ class Tweet(Widget):
class OptionItem(Widget): class OptionItem(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("Option") return Text("Option")
class Error(Widget): class Error(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("This is an error message", justify="center") return Text("This is an error message", justify="center")
class Warning(Widget): class Warning(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("This is a warning message", justify="center") return Text("This is a warning message", justify="center")
class Success(Widget): class Success(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("This is a success message", justify="center") return Text("This is a success message", justify="center")

View File

@@ -13,12 +13,12 @@ lorem = Text.from_markup(
class Lorem(Widget): class Lorem(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Padding(lorem, 1) return Padding(lorem, 1)
class Background(Widget): class Background(Widget):
def render(self, style: Style): def render(self):
return VerticalGradient("#212121", "#212121") return VerticalGradient("#212121", "#212121")

View File

@@ -56,7 +56,7 @@ class Numbers(Widget):
value = Reactive("0") value = Reactive("0")
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
"""Build a Rich renderable to render the calculator display.""" """Build a Rich renderable to render the calculator display."""
return Padding( return Padding(
Align.right(FigletText(self.value), vertical="middle"), Align.right(FigletText(self.value), vertical="middle"),

View File

@@ -1,12 +1,33 @@
site_name: Textual site_name: Textual
site_url: https://www.textualize.io/ site_url: https://www.textualize.io/
nav:
- "index.md"
- "introduction.md"
- Guide:
- "guide/guide.md"
- "actions.md"
- Events:
- "events/mount.md"
- "events/resize.md"
- Styles:
- "styles/width.md"
- Widgets: "/widgets/"
- Reference:
- "reference/app.md"
- "reference/events.md"
- "reference/widget.md"
markdown_extensions: markdown_extensions:
- admonition - admonition
- def_list
- meta - meta
- toc: - toc:
permalink: true permalink: true
baselevel: 1
- pymdownx.keys - pymdownx.keys
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.highlight: - pymdownx.highlight:
anchor_linenums: true anchor_linenums: true
- pymdownx.superfences: - pymdownx.superfences:
@@ -24,6 +45,9 @@ markdown_extensions:
theme: theme:
name: material name: material
custom_dir: custom_theme
# features:
# - navigation.tabs
palette: palette:
- media: "(prefers-color-scheme: light)" - media: "(prefers-color-scheme: light)"
scheme: default scheme: default
@@ -39,13 +63,14 @@ theme:
name: Switch to light mode name: Switch to light mode
plugins: plugins:
- search: - search:
- mkdocstrings: - mkdocstrings:
default_handler: python default_handler: python
handlers: handlers:
python: python:
rendering: rendering:
show_source: false show_source: true
selection: selection:
filters: filters:
- "!^_" - "!^_"

88
poetry.lock generated
View File

@@ -217,6 +217,20 @@ python-dateutil = ">=2.8.1"
[package.extras] [package.extras]
dev = ["twine", "markdown", "flake8", "wheel"] dev = ["twine", "markdown", "flake8", "wheel"]
[[package]]
name = "griffe"
version = "0.19.2"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
cached_property = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
async = ["aiofiles (>=0.7,<1.0)"]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.1" version = "2.5.1"
@@ -342,19 +356,19 @@ mkdocs = ">=1.1"
[[package]] [[package]]
name = "mkdocs-material" name = "mkdocs-material"
version = "7.3.6" version = "8.2.15"
description = "A Material Design theme for MkDocs" description = "Documentation that simply works"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
jinja2 = ">=2.11.1" jinja2 = ">=2.11.1"
markdown = ">=3.2" markdown = ">=3.2"
mkdocs = ">=1.2.3" mkdocs = ">=1.3.0"
mkdocs-material-extensions = ">=1.0" mkdocs-material-extensions = ">=1.0.3"
pygments = ">=2.10" pygments = ">=2.12"
pymdown-extensions = ">=9.0" pymdown-extensions = ">=9.4"
[[package]] [[package]]
name = "mkdocs-material-extensions" name = "mkdocs-material-extensions"
@@ -366,20 +380,50 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "mkdocstrings" name = "mkdocstrings"
version = "0.17.0" version = "0.18.1"
description = "Automatic documentation from sources, for MkDocs." description = "Automatic documentation from sources, for MkDocs."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6.2" python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
Jinja2 = ">=2.11.1" Jinja2 = ">=2.11.1"
Markdown = ">=3.3" Markdown = ">=3.3"
MarkupSafe = ">=1.1" MarkupSafe = ">=1.1"
mkdocs = ">=1.2" mkdocs = ">=1.2"
mkdocs-autorefs = ">=0.1" mkdocs-autorefs = ">=0.3.1"
mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
mkdocstrings-python-legacy = ">=0.2"
pymdown-extensions = ">=6.3" pymdown-extensions = ">=6.3"
pytkdocs = ">=0.14.0"
[package.extras]
crystal = ["mkdocstrings-crystal (>=0.3.4)"]
python = ["mkdocstrings-python (>=0.5.2)"]
python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
[[package]]
name = "mkdocstrings-python"
version = "0.6.6"
description = "A Python handler for mkdocstrings."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
griffe = ">=0.11.1"
mkdocstrings = ">=0.18"
[[package]]
name = "mkdocstrings-python-legacy"
version = "0.2.2"
description = "A legacy Python handler for mkdocstrings."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
mkdocstrings = ">=0.18"
pytkdocs = ">=0.14"
[[package]] [[package]]
name = "msgpack" name = "msgpack"
@@ -773,7 +817,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "eba121f02e102fd9c551a654bcfab3028ec4fc05fe9b4cf7d5f64002e3586ba0" content-hash = "bfa71851a0d29adf2bf6de97054967e2122355bdefe859439bca5ec4377c9992"
[metadata.files] [metadata.files]
aiohttp = [ aiohttp = [
@@ -1039,6 +1083,10 @@ ghp-import = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
] ]
griffe = [
{file = "griffe-0.19.2-py3-none-any.whl", hash = "sha256:5855368feffaabca51be721ca4110220320e3eb361b6c068f2273a6bf7d0bd05"},
{file = "griffe-0.19.2.tar.gz", hash = "sha256:edf925b6ff3101930a97ce48661e34d547b9347fd11e87d49e8cee4c59d30f90"},
]
identify = [ identify = [
{file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"},
{file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"},
@@ -1118,16 +1166,24 @@ mkdocs-autorefs = [
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
] ]
mkdocs-material = [ mkdocs-material = [
{file = "mkdocs-material-7.3.6.tar.gz", hash = "sha256:1b1dbd8ef2508b358d93af55a5c5db3f141c95667fad802301ec621c40c7c217"}, {file = "mkdocs-material-8.2.15.tar.gz", hash = "sha256:93b57e53733051431cc83216446e774bdf08bf516a6251ff2f24974f45f98149"},
{file = "mkdocs_material-7.3.6-py2.py3-none-any.whl", hash = "sha256:1b6b3e9e09f922c2d7f1160fe15c8f43d4adc0d6fb81aa6ff0cbc7ef5b78ec75"}, {file = "mkdocs_material-8.2.15-py2.py3-none-any.whl", hash = "sha256:9d6c4ca1ceecc00b2e38c214665ed7605d275321dcaa22f38b9d1175edc58955"},
] ]
mkdocs-material-extensions = [ mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
{file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"},
] ]
mkdocstrings = [ mkdocstrings = [
{file = "mkdocstrings-0.17.0-py3-none-any.whl", hash = "sha256:103fc1dd58cb23b7e0a6da5292435f01b29dc6fa0ba829132537f3f556f985de"}, {file = "mkdocstrings-0.18.1-py3-none-any.whl", hash = "sha256:4053929356df8cd69ed32eef71d8f676a472ef72980c9ffd4f933ead1debcdad"},
{file = "mkdocstrings-0.17.0.tar.gz", hash = "sha256:75b5cfa2039aeaf3a5f5cf0aa438507b0330ce76c8478da149d692daa7213a98"}, {file = "mkdocstrings-0.18.1.tar.gz", hash = "sha256:fb7c91ce7e3ab70488d3fa6c073a4f827cdc319042f682ef8ea95459790d64fc"},
]
mkdocstrings-python = [
{file = "mkdocstrings-python-0.6.6.tar.gz", hash = "sha256:37281696b9f199624ae420e0625b6659b7fdfbea736618bce7fd978682dea3b1"},
{file = "mkdocstrings_python-0.6.6-py3-none-any.whl", hash = "sha256:c118438d3cb4b14c492a51d109f4e5b27ab06ba19b099d624430dfd904926152"},
]
mkdocstrings-python-legacy = [
{file = "mkdocstrings-python-legacy-0.2.2.tar.gz", hash = "sha256:f0e7ec6a19750581b752acb38f6b32fcd1efe006f14f6703125d2c2c9a5c6f02"},
{file = "mkdocstrings_python_legacy-0.2.2-py3-none-any.whl", hash = "sha256:379107a3a5b8db9b462efc4493c122efe21e825e3702425dbd404621302a563a"},
] ]
msgpack = [ msgpack = [
{file = "msgpack-1.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96acc674bb9c9be63fa8b6dabc3248fdc575c4adc005c440ad02f87ca7edd079"}, {file = "msgpack-1.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96acc674bb9c9be63fa8b6dabc3248fdc575c4adc005c440ad02f87ca7edd079"},

View File

@@ -35,8 +35,8 @@ black = "^22.3.0"
mypy = "^0.950" mypy = "^0.950"
pytest-cov = "^2.12.1" pytest-cov = "^2.12.1"
mkdocs = "^1.3.0" mkdocs = "^1.3.0"
mkdocstrings = "^0.17.0" mkdocstrings = {extras = ["python"], version = "^0.18.1"}
mkdocs-material = "^7.3.6" mkdocs-material = "^8.2.15"
pre-commit = "^2.13.0" pre-commit = "^2.13.0"
pytest-aiohttp = "^1.0.4" pytest-aiohttp = "^1.0.4"
time-machine = "^2.6.0" time-machine = "^2.6.0"

1
reference/README.md Normal file
View File

@@ -0,0 +1 @@
Contains private docs, mainly for the developers reference

View File

@@ -6,7 +6,7 @@ from textual.widgets import Static
class Thing(Widget): class Thing(Widget):
def render(self, style: Style): def render(self):
return "Hello, 3434 World.\n[b]Lorem impsum." return "Hello, 3434 World.\n[b]Lorem impsum."

View File

@@ -56,14 +56,14 @@ lorem_long_text = Text.from_markup(lorem * 2)
class TweetHeader(Widget): class TweetHeader(Widget):
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return Text("Lorem Impsum", justify="center") return Text("Lorem Impsum", justify="center")
class TweetBody(Widget): class TweetBody(Widget):
short_lorem = Reactive[bool](False) short_lorem = Reactive[bool](False)
def render(self, style: Style) -> Text: def render(self) -> Text:
return lorem_short_text if self.short_lorem else lorem_long_text return lorem_short_text if self.short_lorem else lorem_long_text
@@ -72,22 +72,22 @@ class Tweet(Widget):
class OptionItem(Widget): class OptionItem(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("Option") return Text("Option")
class Error(Widget): class Error(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("This is an error message", justify="center") return Text("This is an error message", justify="center")
class Warning(Widget): class Warning(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("This is a warning message", justify="center") return Text("This is a warning message", justify="center")
class Success(Widget): class Success(Widget):
def render(self, style: Style) -> Text: def render(self) -> Text:
return Text("This is a success message", justify="center") return Text("This is a success message", justify="center")

View File

@@ -33,7 +33,7 @@ class Introduction(Widget):
} }
""" """
def render(self, styles) -> RenderableType: def render(self) -> RenderableType:
return Text("Here are the color edge types we support.", justify="center") return Text("Here are the color edge types we support.", justify="center")
@@ -41,7 +41,7 @@ class BorderDemo(Widget):
def __init__(self, name: str): def __init__(self, name: str):
super().__init__(name=name) super().__init__(name=name)
def render(self, style) -> RenderableType: def render(self) -> RenderableType:
return Text(self.name, style="black on yellow", justify="center") return Text(self.name, style="black on yellow", justify="center")

View File

@@ -7,7 +7,7 @@ from textual.widget import Widget
class PanelWidget(Widget): class PanelWidget(Widget):
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return Panel("hello world!", title="Title") return Panel("hello world!", title="Title")

View File

@@ -12,7 +12,7 @@ TEXT = Text.from_markup(lorem)
class TextWidget(Widget): class TextWidget(Widget):
def render(self, style): def render(self):
return TEXT return TEXT

View File

@@ -35,7 +35,7 @@ class Introduction(Widget):
} }
""" """
def render(self, styles) -> RenderableType: def render(self) -> RenderableType:
return Text( return Text(
"Press keys 0 to 9 to scroll to the Placeholder with that ID.", "Press keys 0 to 9 to scroll to the Placeholder with that ID.",
justify="center", justify="center",

View File

@@ -12,7 +12,7 @@ from textual.widgets.tabs import Tabs, Tab
class Hr(Widget): class Hr(Widget):
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return Rule() return Rule()
@@ -23,7 +23,7 @@ class Info(Widget):
super().__init__() super().__init__()
self.text = text self.text = text
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return Padding(f"{self.text}", pad=(0, 1)) return Padding(f"{self.text}", pad=(0, 1))

View File

@@ -221,7 +221,8 @@ class Compositor:
# Keep a copy of the old map because we're going to compare it with the update # Keep a copy of the old map because we're going to compare it with the update
old_map = self.map.copy() old_map = self.map.copy()
old_widgets = old_map.keys() old_widgets = old_map.keys()
map, widgets = self._arrange_root(parent) map, widgets = self._arrange_root(parent, size)
new_widgets = map.keys() new_widgets = map.keys()
# Newly visible widgets # Newly visible widgets
@@ -243,17 +244,16 @@ class Compositor:
resized_widgets = { resized_widgets = {
widget widget
for widget, (region, *_) in map.items() for widget, (region, *_) in map.items()
if widget in old_widgets and widget.size != region.size if widget in old_widgets and old_map[widget].region.size != region.size
} }
# Gets pairs of tuples of (Widget, MapGeometry) which have changed # Gets pairs of tuples of (Widget, MapGeometry) which have changed
# i.e. if something is moved / deleted / added # i.e. if something is moved / deleted / added
screen = size.region screen = size.region
if screen not in self._dirty_regions: if screen not in self._dirty_regions:
crop_screen = screen.intersection crop_screen = screen.intersection
changes: set[tuple[Widget, MapGeometry]] = ( changes = map.items() ^ old_map.items()
self.map.items() ^ old_map.items()
)
self._dirty_regions.update( self._dirty_regions.update(
[ [
crop_screen(map_geometry.visible_region) crop_screen(map_geometry.visible_region)
@@ -267,7 +267,9 @@ class Compositor:
resized=resized_widgets, resized=resized_widgets,
) )
def _arrange_root(self, root: Widget) -> tuple[CompositorMap, set[Widget]]: def _arrange_root(
self, root: Widget, size: Size
) -> tuple[CompositorMap, set[Widget]]:
"""Arrange a widgets children based on its layout attribute. """Arrange a widgets children based on its layout attribute.
Args: Args:
@@ -279,7 +281,7 @@ class Compositor:
""" """
ORIGIN = Offset(0, 0) ORIGIN = Offset(0, 0)
size = root.size
map: CompositorMap = {} map: CompositorMap = {}
widgets: set[Widget] = set() widgets: set[Widget] = set()
get_order = attrgetter("order") get_order = attrgetter("order")
@@ -495,7 +497,7 @@ class Compositor:
"""Get rendered widgets (lists of segments) in the composition. """Get rendered widgets (lists of segments) in the composition.
Returns: Returns:
Iterable[tuple[Region, Region, Lines]]: An interable of <region>, <clip region>, and <lines> Iterable[tuple[Region, Region, Lines]]: An iterable of <region>, <clip region>, and <lines>
""" """
# If a renderable throws an error while rendering, the user likely doesn't care about the traceback # If a renderable throws an error while rendering, the user likely doesn't care about the traceback
# up to this point. # up to this point.
@@ -552,7 +554,7 @@ class Compositor:
] ]
return segment_lines return segment_lines
def render(self, full: bool = True) -> RenderableType: def render(self, full: bool = False) -> RenderableType:
"""Render a layout. """Render a layout.
Returns: Returns:

View File

@@ -21,6 +21,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs):
source = python_code.read() source = python_code.read()
app_vars = {} app_vars = {}
exec(source, app_vars) exec(source, app_vars)
app = app_vars["app"] app = app_vars["app"]
app.run() app.run()
svg = app._screenshot svg = app._screenshot

View File

@@ -34,8 +34,7 @@ import rich.repr
from rich.console import Console, RenderableType from rich.console import Console, RenderableType
from rich.measure import Measurement from rich.measure import Measurement
from rich.protocol import is_renderable from rich.protocol import is_renderable
from rich.segment import Segments from rich.segment import Segments, SegmentLines
from rich.style import Style
from rich.traceback import Traceback from rich.traceback import Traceback
from . import actions from . import actions
@@ -59,6 +58,7 @@ from .geometry import Offset, Region, Size
from .layouts.dock import Dock from .layouts.dock import Dock
from .message_pump import MessagePump from .message_pump import MessagePump
from .reactive import Reactive from .reactive import Reactive
from .renderables.blank import Blank
from .screen import Screen from .screen import Screen
from .widget import Widget from .widget import Widget
@@ -107,7 +107,6 @@ class App(Generic[ReturnType], DOMNode):
"""The base class for Textual Applications""" """The base class for Textual Applications"""
CSS = """ CSS = """
""" """
CSS_PATH: str | None = None CSS_PATH: str | None = None
@@ -209,7 +208,6 @@ class App(Generic[ReturnType], DOMNode):
title: Reactive[str] = Reactive("Textual") title: Reactive[str] = Reactive("Textual")
sub_title: Reactive[str] = Reactive("") sub_title: Reactive[str] = Reactive("")
background: Reactive[str] = Reactive("black")
dark = Reactive(False) dark = Reactive(False)
@property @property
@@ -442,7 +440,8 @@ class App(Generic[ReturnType], DOMNode):
color_system="truecolor", color_system="truecolor",
record=True, record=True,
) )
console.print(self.screen._compositor.render(full=True)) screen_render = self.screen._compositor.render(full=True)
console.print(screen_render)
return console.export_svg(title=self.title) return console.export_svg(title=self.title)
def save_screenshot(self, path: str | None = None) -> str: def save_screenshot(self, path: str | None = None) -> str:
@@ -524,8 +523,8 @@ class App(Generic[ReturnType], DOMNode):
self.stylesheet.update(self) self.stylesheet.update(self)
self.screen.refresh(layout=True) self.screen.refresh(layout=True)
def render(self, styles: Style) -> RenderableType: def render(self) -> RenderableType:
return "" return Blank()
def query(self, selector: str | None = None) -> DOMQuery: def query(self, selector: str | None = None) -> DOMQuery:
"""Get a DOM query in the current screen. """Get a DOM query in the current screen.
@@ -736,21 +735,21 @@ class App(Generic[ReturnType], DOMNode):
driver = self._driver = self.driver_class(self.console, self) driver = self._driver = self.driver_class(self.console, self)
driver.start_application_mode() driver.start_application_mode()
try: with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore
mount_event = events.Mount(sender=self) try:
await self.dispatch_message(mount_event) mount_event = events.Mount(sender=self)
await self.dispatch_message(mount_event)
self.title = self._title self.title = self._title
self.refresh() self.stylesheet.update(self)
await self.animator.start() self.refresh()
await self.animator.start()
with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore
await self._ready() await self._ready()
await super().process_messages() await super().process_messages()
await self.animator.stop() await self.animator.stop()
await self.close_all() await self.close_all()
finally: finally:
driver.stop_application_mode() driver.stop_application_mode()
except Exception as error: except Exception as error:
self.on_exception(error) self.on_exception(error)
finally: finally:
@@ -870,7 +869,11 @@ class App(Generic[ReturnType], DOMNode):
await self.close_messages() await self.close_messages()
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None: def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
self._display(self.screen._compositor) self.screen.refresh(repaint=repaint, layout=layout)
def _paint(self):
"""Perform a "paint" (draw the screen)."""
self._display(self.screen._compositor.render())
def refresh_css(self, animate: bool = True) -> None: def refresh_css(self, animate: bool = True) -> None:
"""Refresh CSS. """Refresh CSS.
@@ -996,7 +999,6 @@ class App(Generic[ReturnType], DOMNode):
action_target = default_namespace or self action_target = default_namespace or self
action_name = target action_name = target
log("<action>", action)
await self.dispatch_action(action_target, action_name, params) await self.dispatch_action(action_target, action_name, params)
async def dispatch_action( async def dispatch_action(
@@ -1011,6 +1013,8 @@ class App(Generic[ReturnType], DOMNode):
_rich_traceback_guard = True _rich_traceback_guard = True
method_name = f"action_{action_name}" method_name = f"action_{action_name}"
method = getattr(namespace, method_name, None) method = getattr(namespace, method_name, None)
if method is None:
log(f"<action> {action_name!r} has no target")
if callable(method): if callable(method):
await invoke(method, *params) await invoke(method, *params)
@@ -1048,11 +1052,11 @@ class App(Generic[ReturnType], DOMNode):
async def handle_update(self, message: messages.Update) -> None: async def handle_update(self, message: messages.Update) -> None:
message.stop() message.stop()
self.app.refresh() self._paint()
async def handle_layout(self, message: messages.Layout) -> None: async def handle_layout(self, message: messages.Layout) -> None:
message.stop() message.stop()
self.app.refresh() self._paint()
async def on_key(self, event: events.Key) -> None: async def on_key(self, event: events.Key) -> None:
if event.key == "tab": if event.key == "tab":
@@ -1067,6 +1071,9 @@ class App(Generic[ReturnType], DOMNode):
await self.close_messages() await self.close_messages()
async def on_resize(self, event: events.Resize) -> None: async def on_resize(self, event: events.Resize) -> None:
event.stop()
self.screen._screen_resized(event.size)
await self.screen.post_message(event) await self.screen.post_message(event)
async def action_press(self, key: str) -> None: async def action_press(self, key: str) -> None:

View File

@@ -1,32 +0,0 @@
from __future__ import annotations
from rich.console import Console, ConsoleOptions
from rich.segment import Segment
from rich.style import StyleType
class Blank:
"""
Render an empty rectangle.
Args:
style (StyleType): Style to apply to the box.
width (int, optional): Width of the box in number of cells. Will expand to fit parent if ``None``.
height (int, optional): Height of the box in number of cells. Will expand to fit parent if ``None``.
"""
def __init__(
self, style: StyleType, width: int | None = None, height: int | None = None
):
self.style = style
self.width = width
self.height = height
def __rich_console__(self, console: Console, console_options: ConsoleOptions):
render_width = self.width or console_options.max_width
render_height = (
self.height or console_options.height or console_options.max_height
)
style = console.get_style(self.style)
for _ in range(render_height):
yield Segment(" " * render_width + "\n", style)

View File

@@ -47,7 +47,7 @@ if TYPE_CHECKING:
from .._layout import Layout from .._layout import Layout
from .styles import DockGroup, Styles, StylesBase from .styles import DockGroup, Styles, StylesBase
from .types import EdgeType from .types import EdgeType, AlignHorizontal, AlignVertical
BorderDefinition = ( BorderDefinition = (
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]" "Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
@@ -935,3 +935,25 @@ class FractionalProperty:
) )
if obj.set_rule(name, clamp(float_value, 0, 1)): if obj.set_rule(name, clamp(float_value, 0, 1)):
obj.refresh() obj.refresh()
class AlignProperty:
"""Combines the horizontal and vertical alignment properties in to a single property."""
def __set_name__(self, owner: StylesBase, name: str) -> None:
self.horizontal = f"{name}_horizontal"
self.vertical = f"{name}_vertical"
def __get__(
self, obj: StylesBase, type: type[StylesBase]
) -> tuple[AlignHorizontal, AlignVertical]:
horizontal = getattr(obj, self.horizontal)
vertical = getattr(obj, self.vertical)
return (horizontal, vertical)
def __set__(
self, obj: StylesBase, value: tuple[AlignHorizontal, AlignVertical]
) -> None:
horizontal, vertical = value
setattr(obj, self.horizontal, horizontal)
setattr(obj, self.vertical, vertical)

View File

@@ -14,6 +14,7 @@ from .._animator import Animation, EasingFunction
from ..color import Color from ..color import Color
from ..geometry import Spacing from ..geometry import Spacing
from ._style_properties import ( from ._style_properties import (
AlignProperty,
BorderProperty, BorderProperty,
BoxProperty, BoxProperty,
ColorProperty, ColorProperty,
@@ -240,9 +241,11 @@ class StylesBase(ABC):
align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left") align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
align = AlignProperty()
content_align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left") content_align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
content_align = AlignProperty()
def __eq__(self, styles: object) -> bool: def __eq__(self, styles: object) -> bool:
"""Check that Styles contains the same rules.""" """Check that Styles contains the same rules."""
@@ -541,7 +544,8 @@ class Styles(StylesBase):
) -> Animation | None: ) -> Animation | None:
from ..widget import Widget from ..widget import Widget
assert isinstance(self.node, Widget) # node = self.node
# assert isinstance(self.node, Widget)
if isinstance(value, ScalarOffset): if isinstance(value, ScalarOffset):
return ScalarAnimation( return ScalarAnimation(
self.node, self.node,

View File

@@ -98,10 +98,12 @@ class Resize(Event, verbosity=2, bubble=False):
container_size: Size | None = None, container_size: Size | None = None,
) -> None: ) -> None:
""" """
Args: Args:
sender (MessageTarget): Event sender. sender (MessageTarget): The sender of the event (the Screen).
width (int): New width in terminal cells. size (Size): The new size of the Widget.
height (int): New height in terminal cells. virtual_size (Size): The virtual size (scrollable size) of the Widget.
container_size (Size | None, optional): The size of the Widget's container widget. Defaults to None.
""" """
self.size = size self.size = size
self.virtual_size = virtual_size self.virtual_size = virtual_size

View File

@@ -1,16 +1,22 @@
from __future__ import annotations from __future__ import annotations
from rich.console import ConsoleOptions, Console, RenderResult from rich.console import ConsoleOptions, Console, RenderResult
from rich.color import Color
from rich.segment import Segment from rich.segment import Segment
from rich.style import Style from rich.style import Style
from ..color import Color
class Blank: class Blank:
"""Draw solid background color.""" """Draw solid background color."""
def __init__(self, color: str) -> None: def __init__(self, color: Color | str = "transparent") -> None:
self._style = Style.from_color(None, Color.parse(color)) background = color if isinstance(color, Color) else Color.parse(color)
self._style = (
Style()
if background.is_transparent
else Style.from_color(None, background.rich_color)
)
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
@@ -18,8 +24,11 @@ class Blank:
width = options.max_width width = options.max_width
height = options.height or options.max_height height = options.height or options.max_height
segment = Segment(f"{' ' * width}\n", self._style) segment = Segment(" " * width, self._style)
yield from [segment] * height line = Segment.line()
for _ in range(height):
yield segment
yield line
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -9,9 +9,11 @@ from rich.style import Style
from . import events, messages, errors from . import events, messages, errors
from .geometry import Offset, Region from .color import Color
from .geometry import Offset, Region, Size
from ._compositor import Compositor, MapGeometry from ._compositor import Compositor, MapGeometry
from .reactive import Reactive from .reactive import Reactive
from .renderables.blank import Blank
from .widget import Widget from .widget import Widget
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
@@ -28,14 +30,10 @@ class Screen(Widget):
"""A widget for the root of the app.""" """A widget for the root of the app."""
CSS = """ CSS = """
Screen { Screen {
layout: vertical; layout: vertical;
overflow-y: auto; overflow-y: auto;
background: $surface;
color: $text-surface;
} }
""" """
dark = Reactive(False) dark = Reactive(False)
@@ -45,11 +43,15 @@ class Screen(Widget):
self._compositor = Compositor() self._compositor = Compositor()
self._dirty_widgets: set[Widget] = set() self._dirty_widgets: set[Widget] = set()
@property
def is_transparent(self) -> bool:
return False
def watch_dark(self, dark: bool) -> None: def watch_dark(self, dark: bool) -> None:
pass pass
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return self.app.render(style) return Blank()
def get_offset(self, widget: Widget) -> Offset: def get_offset(self, widget: Widget) -> Offset:
"""Get the absolute offset of a given Widget. """Get the absolute offset of a given Widget.
@@ -114,15 +116,16 @@ class Screen(Widget):
self._dirty_widgets.clear() self._dirty_widgets.clear()
self._update_timer.pause() self._update_timer.pause()
def _refresh_layout(self) -> None: def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:
"""Refresh the layout (can change size and positions of widgets).""" """Refresh the layout (can change size and positions of widgets)."""
if not self.size: size = self.size if size is None else size
if not size:
return return
# This paint the entire screen, so replaces the batched dirty widgets
self._compositor.update_widgets(self._dirty_widgets) self._compositor.update_widgets(self._dirty_widgets)
self._update_timer.pause() self._update_timer.pause()
try: try:
hidden, shown, resized = self._compositor.reflow(self, self.size) hidden, shown, resized = self._compositor.reflow(self, size)
Hide = events.Hide Hide = events.Hide
Show = events.Show Show = events.Show
@@ -131,6 +134,7 @@ class Screen(Widget):
for widget in shown: for widget in shown:
widget.post_message_no_wait(Show(self)) widget.post_message_no_wait(Show(self))
# We want to send a resize event to widgets that were just added or change since last layout
send_resize = shown | resized send_resize = shown | resized
for ( for (
@@ -151,7 +155,7 @@ class Screen(Widget):
self.app.on_exception(error) self.app.on_exception(error)
return return
display_update = self._compositor.render() display_update = self._compositor.render(full=full)
if display_update is not None: if display_update is not None:
self.app._display(display_update) self.app._display(display_update)
@@ -172,9 +176,11 @@ class Screen(Widget):
UPDATE_PERIOD, self._on_update, name="screen_update", pause=True UPDATE_PERIOD, self._on_update, name="screen_update", pause=True
) )
def _screen_resized(self, size: Size):
"""Called by App when the screen is resized."""
self._refresh_layout(size, full=True)
async def on_resize(self, event: events.Resize) -> None: async def on_resize(self, event: events.Resize) -> None:
self.size_updated(event.size, event.virtual_size, event.container_size)
self._refresh_layout()
event.stop() event.stop()
async def _on_mouse_move(self, event: events.MouseMove) -> None: async def _on_mouse_move(self, event: events.MouseMove) -> None:

View File

@@ -210,7 +210,7 @@ class ScrollBar(Widget):
if self.thickness > 1: if self.thickness > 1:
yield "thickness", self.thickness yield "thickness", self.thickness
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
styles = self.parent.styles styles = self.parent.styles
scrollbar_style = Style( scrollbar_style = Style(
bgcolor=( bgcolor=(

View File

@@ -32,6 +32,7 @@ from .message import Message
from . import messages from . import messages
from ._layout import Layout from ._layout import Layout
from .reactive import Reactive, watch from .reactive import Reactive, watch
from .renderables.blank import Blank
from .renderables.opacity import Opacity from .renderables.opacity import Opacity
from .renderables.tint import Tint from .renderables.tint import Tint
@@ -195,7 +196,7 @@ class Widget(DOMNode):
return self._content_width_cache[1] return self._content_width_cache[1]
console = self.app.console console = self.app.console
renderable = self.render(self.styles.rich_style) renderable = self.render()
measurement = Measurement.get( measurement = Measurement.get(
console, console,
console.options.update_width(container.width), console.options.update_width(container.width),
@@ -233,7 +234,7 @@ class Widget(DOMNode):
if self._content_height_cache[0] == cache_key: if self._content_height_cache[0] == cache_key:
return self._content_height_cache[1] return self._content_height_cache[1]
renderable = self.render(self.styles.rich_style) renderable = self.render()
options = self.console.options.update_width(width).update(highlight=False) options = self.console.options.update_width(width).update(highlight=False)
segments = self.console.render(renderable, options) segments = self.console.render(renderable, options)
# Cheaper than counting the lines returned from render_lines! # Cheaper than counting the lines returned from render_lines!
@@ -684,11 +685,12 @@ class Widget(DOMNode):
Returns: Returns:
RenderableType: A new renderable. RenderableType: A new renderable.
""" """
renderable = self.render(self.text_style)
(base_background, base_color), (background, color) = self.colors (base_background, base_color), (background, color) = self.colors
styles = self.styles styles = self.styles
renderable = self.render()
content_align = (styles.content_align_horizontal, styles.content_align_vertical) content_align = (styles.content_align_horizontal, styles.content_align_vertical)
if content_align != ("left", "top"): if content_align != ("left", "top"):
horizontal, vertical = content_align horizontal, vertical = content_align
@@ -813,7 +815,6 @@ class Widget(DOMNode):
self._size = size self._size = size
self._virtual_size = virtual_size self._virtual_size = virtual_size
self._container_size = container_size self._container_size = container_size
if self.is_container: if self.is_container:
self._refresh_scrollbars() self._refresh_scrollbars()
width, height = self.container_size width, height = self.container_size
@@ -898,7 +899,7 @@ class Widget(DOMNode):
self._repaint_required = True self._repaint_required = True
self.check_idle() self.check_idle()
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
"""Get renderable for widget. """Get renderable for widget.
Args: Args:

View File

@@ -74,7 +74,7 @@ class Button(Widget, can_focus=True):
return Text.from_markup(label) return Text.from_markup(label)
return label return label
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
label = self.label.copy() label = self.label.copy()
label.stylize(style) label.stylize(style)
return label return label

View File

@@ -59,7 +59,7 @@ class Footer(Widget):
text.append_text(key_text) text.append_text(key_text)
return text return text
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
if self._key_text is None: if self._key_text is None:
self._key_text = self.make_key_text() self._key_text = self.make_key_text()
return self._key_text return self._key_text

View File

@@ -49,7 +49,7 @@ class Header(Widget):
def get_clock(self) -> str: def get_clock(self) -> str:
return datetime.now().time().strftime("%X") return datetime.now().time().strftime("%X")
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
header_table = Table.grid(padding=(0, 1), expand=True) header_table = Table.grid(padding=(0, 1), expand=True)
header_table.style = self.style header_table.style = self.style
header_table.add_column(justify="left", ratio=0, width=8) header_table.add_column(justify="left", ratio=0, width=8)

View File

@@ -37,7 +37,7 @@ class Placeholder(Widget, can_focus=True):
yield "has_focus", self.has_focus, False yield "has_focus", self.has_focus, False
yield "mouse_over", self.mouse_over, False yield "mouse_over", self.mouse_over, False
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
# Apply colours only inside render_styled # Apply colours only inside render_styled
# Pass the full RICH style object into `render` - not the `Styles` # Pass the full RICH style object into `render` - not the `Styles`
return Panel( return Panel(

View File

@@ -9,7 +9,7 @@ from ..widget import Widget
class Static(Widget): class Static(Widget):
def __init__( def __init__(
self, self,
renderable: RenderableType, renderable: RenderableType = "",
*, *,
name: str | None = None, name: str | None = None,
id: str | None = None, id: str | None = None,
@@ -18,7 +18,7 @@ class Static(Widget):
super().__init__(name=name, id=id, classes=classes) super().__init__(name=name, id=id, classes=classes)
self.renderable = renderable self.renderable = renderable
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return self.renderable return self.renderable
def update(self, renderable: RenderableType) -> None: def update(self, renderable: RenderableType) -> None:

View File

@@ -248,7 +248,7 @@ class TreeControl(Generic[NodeDataType], Widget):
push(iter(node.children)) push(iter(node.children))
return None return None
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return self._tree return self._tree
def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType: def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType:

View File

@@ -330,7 +330,7 @@ class Tabs(Widget):
""" """
return next((i for i, tab in enumerate(self.tabs) if tab.name == tab_name), 0) return next((i for i, tab in enumerate(self.tabs) if tab.name == tab_name), 0)
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
return TabsRenderable( return TabsRenderable(
self.tabs, self.tabs,
tab_padding=self.tab_padding, tab_padding=self.tab_padding,

View File

@@ -220,7 +220,7 @@ class TextInput(TextWidgetBase, can_focus=True):
self.refresh() self.refresh()
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
# First render: Cursor at start of text, visible range goes from cursor to content region width # First render: Cursor at start of text, visible range goes from cursor to content region width
if not self.visible_range: if not self.visible_range:
self.visible_range = (self._editor.cursor_index, self.content_region.width) self.visible_range = (self._editor.cursor_index, self.content_region.width)
@@ -400,7 +400,7 @@ class TextAreaChild(TextWidgetBase, can_focus=True):
CSS = "TextAreaChild { height: auto; background: $primary-darken-1; }" CSS = "TextAreaChild { height: auto; background: $primary-darken-1; }"
STOP_PROPAGATE = {"tab", "shift+tab"} STOP_PROPAGATE = {"tab", "shift+tab"}
def render(self, style: Style) -> RenderableType: def render(self) -> RenderableType:
# We only show the cursor if the widget has focus # We only show the cursor if the widget has focus
show_cursor = self.has_focus show_cursor = self.has_focus
display_text = Text(self._editor.content, no_wrap=True) display_text = Text(self._editor.content, no_wrap=True)

View File

@@ -252,7 +252,7 @@ async def test_scrollbar_gutter(
from textual.geometry import Size from textual.geometry import Size
class TextWidget(Widget): class TextWidget(Widget):
def render(self, styles) -> Text: def render(self) -> Text:
text_multiplier = 10 if text_length == "long_text" else 2 text_multiplier = 10 if text_length == "long_text" else 2
return Text( return Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a." "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a."

View File

@@ -195,7 +195,7 @@ async def test_border_edge_types_impact_on_widget_size(
expects_visible_char_at_top_left_edge: bool, expects_visible_char_at_top_left_edge: bool,
): ):
class BorderTarget(Widget): class BorderTarget(Widget):
def render(self, style) -> RenderableType: def render(self) -> RenderableType:
return Text("border target", style="black on yellow", justify="center") return Text("border target", style="black on yellow", justify="center")
border_target = BorderTarget() border_target = BorderTarget()

View File

@@ -39,7 +39,7 @@ def test_widget_content_width():
self.text = text self.text = text
super().__init__(id=id) super().__init__(id=id)
def render(self, style: Style) -> str: def render(self) -> str:
return self.text return self.text
widget1 = TextWidget("foo", id="widget1") widget1 = TextWidget("foo", id="widget1")