mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into add-containers
This commit is contained in:
1
docs/api/content_switcher.md
Normal file
1
docs/api/content_switcher.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.ContentSwitcher
|
||||
76
docs/blog/posts/creating-tasks-overhead.md
Normal file
76
docs/blog/posts/creating-tasks-overhead.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
draft: false
|
||||
date: 2023-03-08
|
||||
categories:
|
||||
- DevLog
|
||||
authors:
|
||||
- willmcgugan
|
||||
---
|
||||
|
||||
# Overhead of Python Asyncio tasks
|
||||
|
||||
Every widget in Textual, be it a button, tree view, or a text input, runs an [asyncio](https://docs.python.org/3/library/asyncio.html) task. There is even a task for [scrollbar corners](https://github.com/Textualize/textual/blob/e95a65fa56e5b19715180f9e17c7f6747ba15ec5/src/textual/scrollbar.py#L365) (the little space formed when horizontal and vertical scrollbars meet).
|
||||
|
||||
<!-- more -->
|
||||
|
||||
!!! info
|
||||
|
||||
It may be IO that gives AsyncIO its name, but Textual doesn't do any IO of its own. Those tasks are used to power *message queues*, so that widgets (UI components) can do whatever they do at their own pace.
|
||||
|
||||
Its fair to say that Textual apps launch a lot of tasks. Which is why when I was trying to optimize startup (for apps with 1000s of widgets) I suspected it was task related.
|
||||
|
||||
I needed to know how much of an overhead it was to launch tasks. Tasks are lighter weight than threads, but how much lighter? The only way to know for certain was to profile.
|
||||
|
||||
The following code launches a load of *do nothing* tasks, then waits for them to shut down. This would give me an idea of how performant `create_task` is, and also a *baseline* for optimizations. I would know the absolute limit of any optimizations I make.
|
||||
|
||||
```python
|
||||
from asyncio import create_task, wait, run
|
||||
from time import process_time as time
|
||||
|
||||
|
||||
async def time_tasks(count=100) -> float:
|
||||
"""Time creating and destroying tasks."""
|
||||
|
||||
async def nop_task() -> None:
|
||||
"""Do nothing task."""
|
||||
pass
|
||||
|
||||
start = time()
|
||||
tasks = [create_task(nop_task()) for _ in range(count)]
|
||||
await wait(tasks)
|
||||
elapsed = time() - start
|
||||
return elapsed
|
||||
|
||||
|
||||
for count in range(100_000, 1000_000 + 1, 100_000):
|
||||
create_time = run(time_tasks(count))
|
||||
create_per_second = 1 / (create_time / count)
|
||||
print(f"{count:,} tasks \t {create_per_second:0,.0f} tasks per/s")
|
||||
```
|
||||
|
||||
And here is the output:
|
||||
|
||||
```
|
||||
100,000 tasks 280,003 tasks per/s
|
||||
200,000 tasks 255,275 tasks per/s
|
||||
300,000 tasks 248,713 tasks per/s
|
||||
400,000 tasks 248,383 tasks per/s
|
||||
500,000 tasks 241,624 tasks per/s
|
||||
600,000 tasks 260,660 tasks per/s
|
||||
700,000 tasks 244,510 tasks per/s
|
||||
800,000 tasks 247,455 tasks per/s
|
||||
900,000 tasks 242,744 tasks per/s
|
||||
1,000,000 tasks 259,715 tasks per/s
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
Running on an M1 MacBook Pro.
|
||||
|
||||
This tells me I can create, run, and shutdown 260K tasks per second.
|
||||
|
||||
That's fast.
|
||||
|
||||
Clearly `create_task` is as close as you get to free in the Python world, and I would need to look elsewhere for optimizations. Turns out Textual spends far more time processing CSS rules than creating tasks (obvious in retrospect). I've noticed some big wins there, so the next version of Textual will be faster to start apps with a metric tonne of widgets.
|
||||
|
||||
But I still need to know what to do with those scrollbar corners. A task for two characters. I don't even...
|
||||
77
docs/blog/posts/release0-14-0.md
Normal file
77
docs/blog/posts/release0-14-0.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
draft: false
|
||||
date: 2023-03-09
|
||||
categories:
|
||||
- Release
|
||||
title: "Textual 0.14.0 shakes up posting messages"
|
||||
authors:
|
||||
- willmcgugan
|
||||
---
|
||||
|
||||
# Textual 0.14.0 shakes up posting messages
|
||||
|
||||
Textual version 0.14.0 has landed just a week after 0.13.0.
|
||||
|
||||
!!! note
|
||||
|
||||
We like fast releases for Textual. Fast releases means quicker feedback, which means better code.
|
||||
|
||||
What's new?
|
||||
|
||||
<!-- more -->
|
||||
|
||||
We did a little shake-up of posting [messages](../../guide/events.md) which will simplify building widgets. But this does mean a few breaking changes.
|
||||
|
||||
There are two methods in Textual to post messages: `post_message` and `post_message_no_wait`. The former was asynchronous (you needed to `await` it), and the latter was a regular method call. These two methods have been replaced with a single `post_message` method.
|
||||
|
||||
To upgrade your project to Textual 0.14.0, you will need to do the following:
|
||||
|
||||
- Remove `await` keywords from any calls to `post_message`.
|
||||
- Replace any calls to `post_message_no_wait` with `post_message`.
|
||||
|
||||
|
||||
Additionally, we've simplified constructing messages classes. Previously all messages required a `sender` argument, which had to be manually set. This was a clear violation of our "no boilerplate" policy, and has been dropped. There is still a `sender` property on messages / events, but it is set automatically.
|
||||
|
||||
So prior to 0.14.0 you might have posted messages like the following:
|
||||
|
||||
```python
|
||||
async self.post_message(self.Changed(self, item=self.item))
|
||||
```
|
||||
|
||||
You can now replace it with this simpler function call:
|
||||
|
||||
```python
|
||||
self.post_message(self.Change(item=self.item))
|
||||
```
|
||||
|
||||
This also means that you will need to drop the sender from any custom messages you have created.
|
||||
|
||||
If this was code pre-0.14.0:
|
||||
|
||||
```python
|
||||
class MyWidget(Widget):
|
||||
|
||||
class Changed(Message):
|
||||
"""My widget change event."""
|
||||
def __init__(self, sender:MessageTarget, item_index:int) -> None:
|
||||
self.item_index = item_index
|
||||
super().__init__(sender)
|
||||
|
||||
```
|
||||
|
||||
You would need to make the following change (dropping `sender`).
|
||||
|
||||
```python
|
||||
class MyWidget(Widget):
|
||||
|
||||
class Changed(Message):
|
||||
"""My widget change event."""
|
||||
def __init__(self, item_index:int) -> None:
|
||||
self.item_index = item_index
|
||||
super().__init__()
|
||||
|
||||
```
|
||||
|
||||
If you have any problems upgrading, join our [Discord server](https://discord.gg/Enf6Z3qhVr), we would be happy to help.
|
||||
|
||||
See the [release notes](https://github.com/Textualize/textual/releases/tag/v0.14.0) for the full details on this update.
|
||||
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-Bold.ttf
Normal file
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-Bold.ttf
Normal file
Binary file not shown.
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-BoldItalic.ttf
Normal file
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-Italic.ttf
Normal file
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-Italic.ttf
Normal file
Binary file not shown.
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-Regular.ttf
Normal file
BIN
docs/custom_theme/assets/fonts/BerkeleyMono-Regular.ttf
Normal file
Binary file not shown.
@@ -42,7 +42,7 @@ class CompoundApp(App):
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield InputWithLabel("Fist Name")
|
||||
yield InputWithLabel("First Name")
|
||||
yield InputWithLabel("Last Name")
|
||||
yield InputWithLabel("Email")
|
||||
|
||||
|
||||
27
docs/examples/widgets/content_switcher.css
Normal file
27
docs/examples/widgets/content_switcher.css
Normal file
@@ -0,0 +1,27 @@
|
||||
Screen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
margin-top: 1;
|
||||
height: 3;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
ContentSwitcher {
|
||||
background: $panel;
|
||||
border: round $primary;
|
||||
width: 90%;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
DataTable {
|
||||
background: $panel;
|
||||
}
|
||||
|
||||
MarkdownH2 {
|
||||
background: $primary;
|
||||
color: yellow;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
64
docs/examples/widgets/content_switcher.py
Normal file
64
docs/examples/widgets/content_switcher.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal
|
||||
from textual.widgets import Button, ContentSwitcher, DataTable, Markdown
|
||||
|
||||
MARKDOWN_EXAMPLE = """# Three Flavours Cornetto
|
||||
|
||||
The Three Flavours Cornetto trilogy is an anthology series of British
|
||||
comedic genre films directed by Edgar Wright.
|
||||
|
||||
## Shaun of the Dead
|
||||
|
||||
| Flavour | UK Release Date | Director |
|
||||
| -- | -- | -- |
|
||||
| Strawberry | 2004-04-09 | Edgar Wright |
|
||||
|
||||
## Hot Fuzz
|
||||
|
||||
| Flavour | UK Release Date | Director |
|
||||
| -- | -- | -- |
|
||||
| Classico | 2007-02-17 | Edgar Wright |
|
||||
|
||||
## The World's End
|
||||
|
||||
| Flavour | UK Release Date | Director |
|
||||
| -- | -- | -- |
|
||||
| Mint | 2013-07-19 | Edgar Wright |
|
||||
"""
|
||||
|
||||
|
||||
class ContentSwitcherApp(App[None]):
|
||||
CSS_PATH = "content_switcher.css"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(id="buttons"): # (1)!
|
||||
yield Button("DataTable", id="data-table") # (2)!
|
||||
yield Button("Markdown", id="markdown") # (3)!
|
||||
|
||||
with ContentSwitcher(initial="data-table"): # (4)!
|
||||
yield DataTable(id="data-table")
|
||||
yield Markdown(MARKDOWN_EXAMPLE, id="markdown")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.query_one(ContentSwitcher).current = event.button.id # (5)!
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one(DataTable)
|
||||
table.add_columns("Book", "Year")
|
||||
table.add_rows(
|
||||
[
|
||||
(title.ljust(35), year)
|
||||
for title, year in (
|
||||
("Dune", 1965),
|
||||
("Dune Messiah", 1969),
|
||||
("Children of Dune", 1976),
|
||||
("God Emperor of Dune", 1981),
|
||||
("Heretics of Dune", 1984),
|
||||
("Chapterhouse: Dune", 1985),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ContentSwitcherApp().run()
|
||||
@@ -71,7 +71,7 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t
|
||||
|
||||
|
||||
|
||||
```{.textual path="examples/calculator.py" columns=100 lines=41 press="3,.,1,4,5,9,2,_,_,_,_,_,_,_,_"}
|
||||
```{.textual path="examples/calculator.py" columns=100 lines=41 press="3,.,1,4,5,9,2,wait:400"}
|
||||
```
|
||||
|
||||
```{.textual path="examples/pride.py"}
|
||||
|
||||
@@ -41,7 +41,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
|
||||
* [x] Error / warning variants
|
||||
- [ ] Color picker
|
||||
- [X] Checkbox
|
||||
- [ ] Content switcher
|
||||
- [X] Content switcher
|
||||
- [x] DataTable
|
||||
* [x] Cell select
|
||||
* [x] Row / Column select
|
||||
|
||||
@@ -35,6 +35,14 @@ A classic checkbox control.
|
||||
```
|
||||
|
||||
|
||||
## ContentSwitcher
|
||||
|
||||
A widget for containing and switching display between multiple child
|
||||
widgets.
|
||||
|
||||
[ContentSwitcher reference](./widgets/content_switcher.md){ .md-button .md-button--primary }
|
||||
|
||||
|
||||
## DataTable
|
||||
|
||||
A powerful data table, with configurable cursors.
|
||||
|
||||
@@ -10,7 +10,7 @@ when it has focus.
|
||||
## Example
|
||||
|
||||
The example below shows each button variant, and its disabled equivalent.
|
||||
Clicking any of the non-disabled buttons in the example app below will result the app exiting and the details of the selected button being printed to the console.
|
||||
Clicking any of the non-disabled buttons in the example app below will result in the app exiting and the details of the selected button being printed to the console.
|
||||
|
||||
=== "Output"
|
||||
|
||||
|
||||
54
docs/widgets/content_switcher.md
Normal file
54
docs/widgets/content_switcher.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# ContentSwitcher
|
||||
|
||||
A widget for containing and switching display between multiple child
|
||||
widgets.
|
||||
|
||||
- [ ] Focusable
|
||||
- [X] Container
|
||||
|
||||
## Example
|
||||
|
||||
The example below uses a `ContentSwitcher` in combination with two `Button`s
|
||||
to create a simple tabbed view. Note how each `Button` has an ID set, and
|
||||
how each child of the `ContentSwitcher` has a corresponding ID; then a
|
||||
`Button.Clicked` handler is used to set `ContentSwitcher.current` to switch
|
||||
between the different views.
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/widgets/content_switcher.py"}
|
||||
```
|
||||
|
||||
=== "content_switcher.py"
|
||||
|
||||
~~~python
|
||||
--8<-- "docs/examples/widgets/content_switcher.py"
|
||||
~~~
|
||||
|
||||
1. A `Horizontal` to hold the buttons, each with a unique ID.
|
||||
2. This button will select the `DataTable` in the `ContentSwitcher`.
|
||||
3. This button will select the `Markdown` in the `ContentSwitcher`.
|
||||
4. Note that the intial visible content is set by its ID, see below.
|
||||
5. When a button is pressed, its ID is used to switch to a different widget in the `ContentSwitcher`. Remember that IDs are unique within parent, so the buttons and the widgets in the `ContentSwitcher` can share IDs.
|
||||
|
||||
=== "content_switcher.css"
|
||||
|
||||
~~~sass
|
||||
--8<-- "docs/examples/widgets/content_switcher.css"
|
||||
~~~
|
||||
|
||||
When the user presses the "Markdown" button the view is switched:
|
||||
|
||||
```{.textual path="docs/examples/widgets/content_switcher.py" lines="40" press="tab,tab,enter"}
|
||||
```
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------|-----------------|---------|----------------------------------------------------------------------|
|
||||
| `current` | `str` \| `None` | `None` | The ID of the currently-visible child. `None` means nothing is visible. |
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
* [ContentSwitcher][textual.widgets.ContentSwitcher] code reference
|
||||
@@ -1,4 +1,4 @@
|
||||
# List Item
|
||||
# ListItem
|
||||
|
||||
`ListItem` is the type of the elements in a `ListView`.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# List View
|
||||
# ListView
|
||||
|
||||
Displays a vertical list of `ListItem`s which can be highlighted and selected.
|
||||
Supports keyboard navigation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Markdown Viewer
|
||||
# MarkdownViewer
|
||||
|
||||
A Widget to display Markdown content with an optional Table of Contents.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user