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:
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## [0.15.0] - Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -14,11 +14,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added a LoadingIndicator widget https://github.com/Textualize/textual/pull/2018
|
||||||
- Added `HorizontalScroll` https://github.com/Textualize/textual/issues/1957
|
- Added `HorizontalScroll` https://github.com/Textualize/textual/issues/1957
|
||||||
- Added `Center` https://github.com/Textualize/textual/issues/1957
|
- Added `Center` https://github.com/Textualize/textual/issues/1957
|
||||||
- Added `Middle` https://github.com/Textualize/textual/issues/1957
|
- Added `Middle` https://github.com/Textualize/textual/issues/1957
|
||||||
- Added `VerticalScroll` (mimicking the old behaviour of `Vertical`) https://github.com/Textualize/textual/issues/1957
|
- Added `VerticalScroll` (mimicking the old behaviour of `Vertical`) https://github.com/Textualize/textual/issues/1957
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed container not resizing when a widget is removed https://github.com/Textualize/textual/issues/2007
|
||||||
|
- Fixes issue where the horizontal scrollbar would be incorrectly enabled https://github.com/Textualize/textual/pull/2024
|
||||||
|
|
||||||
## [0.14.0] - 2023-03-09
|
## [0.14.0] - 2023-03-09
|
||||||
|
|
||||||
|
|||||||
1
docs/api/loading_indicator.md
Normal file
1
docs/api/loading_indicator.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
::: textual.widgets.LoadingIndicator
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual.containers import VerticalScroll
|
from textual.containers import Container
|
||||||
from textual.widgets import Label
|
from textual.widgets import Label
|
||||||
|
|
||||||
TEXT = """I must not fear.
|
TEXT = """I must not fear.
|
||||||
@@ -14,7 +14,7 @@ Where the fear has gone there will be nothing. Only I will remain.
|
|||||||
|
|
||||||
class ScrollbarApp(App):
|
class ScrollbarApp(App):
|
||||||
def compose(self):
|
def compose(self):
|
||||||
yield VerticalScroll(Label(TEXT * 5), classes="panel")
|
yield Container(Label(TEXT * 5), classes="panel")
|
||||||
|
|
||||||
|
|
||||||
app = ScrollbarApp(css_path="scrollbar_size.css")
|
app = ScrollbarApp(css_path="scrollbar_size.css")
|
||||||
|
|||||||
12
docs/examples/widgets/loading_indicator.py
Normal file
12
docs/examples/widgets/loading_indicator.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import LoadingIndicator
|
||||||
|
|
||||||
|
|
||||||
|
class LoadingApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield LoadingIndicator()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = LoadingApp()
|
||||||
|
app.run()
|
||||||
@@ -44,7 +44,7 @@ python -m textual
|
|||||||
|
|
||||||
If Textual is installed you should see the following:
|
If Textual is installed you should see the following:
|
||||||
|
|
||||||
```{.textual path="src/textual/demo.py" columns="127" lines="53" press="enter,_,_,_,_,_,_,tab,_,w,i,l,l"}
|
```{.textual path="src/textual/demo.py" columns="127" lines="53" press="enter,tab,w,i,l,l"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Let's look at a trivial Textual app.
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/dom1.py" press="_"}
|
```{.textual path="docs/examples/guide/dom1.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
This example creates an instance of `ExampleApp`, which will implicitly create a `Screen` object. In DOM terms, the `Screen` is a _child_ of `ExampleApp`.
|
This example creates an instance of `ExampleApp`, which will implicitly create a `Screen` object. In DOM terms, the `Screen` is a _child_ of `ExampleApp`.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Apps don't get much simpler than this—don't expect it to do much.
|
|||||||
|
|
||||||
If we run this app with `python simple02.py` you will see a blank terminal, something like the following:
|
If we run this app with `python simple02.py` you will see a blank terminal, something like the following:
|
||||||
|
|
||||||
```{.textual path="docs/examples/app/simple02.py" press="_"}
|
```{.textual path="docs/examples/app/simple02.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*).
|
When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*).
|
||||||
@@ -57,7 +57,7 @@ Another such event is the *key* event which is sent when the user presses a key.
|
|||||||
|
|
||||||
The `on_mount` handler sets the `self.screen.styles.background` attribute to `"darkblue"` which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run this code.
|
The `on_mount` handler sets the `self.screen.styles.background` attribute to `"darkblue"` which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run this code.
|
||||||
|
|
||||||
```{.textual path="docs/examples/app/event01.py" hl_lines="23-25" press="_"}
|
```{.textual path="docs/examples/app/event01.py" hl_lines="23-25"}
|
||||||
```
|
```
|
||||||
|
|
||||||
The key event handler (`on_key`) has an `event` parameter which will receive a [Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.
|
The key event handler (`on_key`) has an `event` parameter which will receive a [Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.
|
||||||
@@ -114,7 +114,7 @@ Here's an app which adds a welcome widget in response to any key press:
|
|||||||
|
|
||||||
When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets.
|
When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets.
|
||||||
|
|
||||||
```{.textual path="docs/examples/app/widgets02.py" press="a,a,a,down,down,down,down,down,down,_,_,_,_,_,_"}
|
```{.textual path="docs/examples/app/widgets02.py" press="a,a,a,down,down,down,down,down,down"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Exiting
|
## Exiting
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ textual console
|
|||||||
|
|
||||||
You should see the Textual devtools welcome message:
|
You should see the Textual devtools welcome message:
|
||||||
|
|
||||||
```{.textual title="textual console" path="docs/examples/getting_started/console.py", press="_,_"}
|
```{.textual title="textual console" path="docs/examples/getting_started/console.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
In the other console, run your application with `textual run` and the `--dev` switch:
|
In the other console, run your application with `textual run` and the `--dev` switch:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ The most fundamental way to receive input is via [Key][textual.events.Key] event
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"}
|
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!"}
|
||||||
```
|
```
|
||||||
|
|
||||||
When you press a key, the app will receive the event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens.
|
When you press a key, the app will receive the event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens.
|
||||||
@@ -102,7 +102,7 @@ The following example shows how focus works in practice.
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/input/key03.py", press="tab,H,e,l,l,o,tab,W,o,r,l,d,!,_"}
|
```{.textual path="docs/examples/guide/input/key03.py", press="tab,H,e,l,l,o,tab,W,o,r,l,d,!"}
|
||||||
```
|
```
|
||||||
|
|
||||||
The app splits the screen in to quarters, with a `TextLog` widget in each quarter. If you click any of the text logs, you should see that it is highlighted to show that the widget has focus. Key events will be sent to the focused widget only.
|
The app splits the screen in to quarters, with a `TextLog` widget in each quarter. If you click any of the text logs, you should see that it is highlighted to show that the widget has focus. Key events will be sent to the focused widget only.
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ The code below shows a simple sidebar implementation.
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down,_,_,_,_,_"}
|
```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down"}
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "dock_layout1_sidebar.py"
|
=== "dock_layout1_sidebar.py"
|
||||||
@@ -468,7 +468,7 @@ This new sidebar is double the width of the one previous one, and has a `deeppin
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/layout/dock_layout2_sidebar.py" press="pagedown,down,down,_,_,_,_,_"}
|
```{.textual path="docs/examples/guide/layout/dock_layout2_sidebar.py" press="pagedown,down,down"}
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "dock_layout2_sidebar.py"
|
=== "dock_layout2_sidebar.py"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Let's look at a simple example of writing a screen class to simulate Window's [b
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/screens/screen01.py" press="b,_"}
|
```{.textual path="docs/examples/guide/screens/screen01.py" press="b"}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you run this you will see an empty screen. Hit the ++b++ key to show a blue screen of death. Hit ++escape++ to return to the default screen.
|
If you run this you will see an empty screen. Hit the ++b++ key to show a blue screen of death. Hit ++escape++ to return to the default screen.
|
||||||
@@ -65,7 +65,7 @@ You can also _install_ new named screens dynamically with the [install_screen][t
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/screens/screen02.py" press="b,_"}
|
```{.textual path="docs/examples/guide/screens/screen02.py" press="b"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Although both do the same thing, we recommend `SCREENS` for screens that exist for the lifetime of your app.
|
Although both do the same thing, we recommend `SCREENS` for screens that exist for the lifetime of your app.
|
||||||
@@ -145,7 +145,7 @@ Screens can be used to implement modal dialogs. The following example pushes a s
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/screens/modal01.py" press="q,_"}
|
```{.textual path="docs/examples/guide/screens/modal01.py" press="q"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note the `request_quit` action in the app which pushes a new instance of `QuitScreen`. This makes the quit screen active. if you click cancel, the quit screen calls `pop_screen` to return the default screen. This also removes and deletes the `QuitScreen` object.
|
Note the `request_quit` action in the app which pushes a new instance of `QuitScreen`. This makes the quit screen active. if you click cancel, the quit screen calls `pop_screen` to return the default screen. This also removes and deletes the `QuitScreen` object.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ The first line sets the [background](../styles/background.md) style to `"darkblu
|
|||||||
|
|
||||||
The second line sets [border](../styles/border.md) to a tuple of `("heavy", "white")` which tells Textual to draw a white border with a style of `"heavy"`. Running this code will show the following:
|
The second line sets [border](../styles/border.md) to a tuple of `("heavy", "white")` which tells Textual to draw a white border with a style of `"heavy"`. Running this code will show the following:
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/styles/screen.py" press="_"}
|
```{.textual path="docs/examples/guide/styles/screen.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Styling widgets
|
## Styling widgets
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ Let's use markup links in the hello example so that the greeting becomes a link
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/widgets/hello05.py" press="_"}
|
```{.textual path="docs/examples/guide/widgets/hello05.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the `next_word` action which updates the next word.
|
If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the `next_word` action which updates the next word.
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t
|
|||||||
```{.textual path="examples/pride.py"}
|
```{.textual path="examples/pride.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
```{.textual path="docs/examples/tutorial/stopwatch.py" columns="100" lines="30" press="d,tab,enter,_,_"}
|
```{.textual path="docs/examples/tutorial/stopwatch.py" columns="100" lines="30" press="d,tab,enter"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Notice that even though the content is scrolled, the sidebar remains fixed.
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down,_,_,_,_,_"}
|
```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down"}
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "dock_layout1_sidebar.py"
|
=== "dock_layout1_sidebar.py"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ This will be a simple yet **fully featured** app — you could distribute th
|
|||||||
Here's what the finished app will look like:
|
Here's what the finished app will look like:
|
||||||
|
|
||||||
|
|
||||||
```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,_,tab,enter,_,tab,_,enter,_,tab,enter,_,_"}
|
```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,tab,enter,tab,enter,tab,enter"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Get the code
|
### Get the code
|
||||||
@@ -339,7 +339,7 @@ The `on_button_pressed` method is an *event handler*. Event handlers are methods
|
|||||||
|
|
||||||
If you run `stopwatch04.py` now you will be able to toggle between the two states by clicking the first button:
|
If you run `stopwatch04.py` now you will be able to toggle between the two states by clicking the first button:
|
||||||
|
|
||||||
```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,_,enter,_,_,_"}
|
```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reactive attributes
|
## Reactive attributes
|
||||||
@@ -421,7 +421,7 @@ This code supplies missing features and makes our app useful. We've made the fol
|
|||||||
|
|
||||||
If you run `stopwatch06.py` you will be able to use the stopwatches independently.
|
If you run `stopwatch06.py` you will be able to use the stopwatches independently.
|
||||||
|
|
||||||
```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"}
|
```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,tab,enter,tab"}
|
||||||
```
|
```
|
||||||
|
|
||||||
The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches.
|
The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches.
|
||||||
@@ -449,7 +449,7 @@ The `action_remove_stopwatch` function calls [query()][textual.dom.DOMNode.query
|
|||||||
|
|
||||||
If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and remove a stopwatch with ++r++.
|
If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and remove a stopwatch with ++r++.
|
||||||
|
|
||||||
```{.textual path="docs/examples/tutorial/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,_,_,_,_,tab,_"}
|
```{.textual path="docs/examples/tutorial/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,tab"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## What next?
|
## What next?
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ Display a list of items (items may be other widgets).
|
|||||||
```{.textual path="docs/examples/widgets/list_view.py"}
|
```{.textual path="docs/examples/widgets/list_view.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## LoadingIndicator
|
||||||
|
|
||||||
|
Display an animation while data is loading.
|
||||||
|
|
||||||
|
[LoadingIndicator reference](./widgets/loading_indicator.md){ .md-button .md-button--primary }
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/widgets/loading_indicator.py"}
|
||||||
|
```
|
||||||
|
|
||||||
## MarkdownViewer
|
## MarkdownViewer
|
||||||
|
|
||||||
Display and interact with a Markdown document (adds a table of contents and browser-like navigation to Markdown).
|
Display and interact with a Markdown document (adds a table of contents and browser-like navigation to Markdown).
|
||||||
@@ -182,7 +191,7 @@ Display and update text in a scrolling panel.
|
|||||||
|
|
||||||
[TextLog reference](./widgets/text_log.md){ .md-button .md-button--primary }
|
[TextLog reference](./widgets/text_log.md){ .md-button .md-button--primary }
|
||||||
|
|
||||||
```{.textual path="docs/examples/widgets/text_log.py" press="_,H,i"}
|
```{.textual path="docs/examples/widgets/text_log.py" press="H,i"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tree
|
## Tree
|
||||||
|
|||||||
24
docs/widgets/loading_indicator.md
Normal file
24
docs/widgets/loading_indicator.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# LoadingIndicator
|
||||||
|
|
||||||
|
Displays pulsating dots to indicate when data is being loaded.
|
||||||
|
|
||||||
|
- [ ] Focusable
|
||||||
|
- [ ] Container
|
||||||
|
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/widgets/loading_indicator.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "loading_indicator.py"
|
||||||
|
|
||||||
|
```python
|
||||||
|
--8<-- "docs/examples/widgets/loading_indicator.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [LoadingIndicator](../api/loading_indicator.md) code reference
|
||||||
@@ -13,7 +13,7 @@ The example below shows an application showing a `TextLog` with different kinds
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/widgets/text_log.py" press="_,H,i"}
|
```{.textual path="docs/examples/widgets/text_log.py" press="H,i"}
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "text_log.py"
|
=== "text_log.py"
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ nav:
|
|||||||
- "widgets/label.md"
|
- "widgets/label.md"
|
||||||
- "widgets/list_item.md"
|
- "widgets/list_item.md"
|
||||||
- "widgets/list_view.md"
|
- "widgets/list_view.md"
|
||||||
|
- "widgets/loading_indicator.md"
|
||||||
- "widgets/markdown_viewer.md"
|
- "widgets/markdown_viewer.md"
|
||||||
- "widgets/markdown.md"
|
- "widgets/markdown.md"
|
||||||
- "widgets/placeholder.md"
|
- "widgets/placeholder.md"
|
||||||
@@ -163,6 +164,7 @@ nav:
|
|||||||
- "api/label.md"
|
- "api/label.md"
|
||||||
- "api/list_item.md"
|
- "api/list_item.md"
|
||||||
- "api/list_view.md"
|
- "api/list_view.md"
|
||||||
|
- "api/loading_indicator.md"
|
||||||
- "api/markdown_viewer.md"
|
- "api/markdown_viewer.md"
|
||||||
- "api/markdown.md"
|
- "api/markdown.md"
|
||||||
- "api/message_pump.md"
|
- "api/message_pump.md"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ typing-extensions = "^4.4.0"
|
|||||||
aiohttp = { version = ">=3.8.1", optional = true }
|
aiohttp = { version = ">=3.8.1", optional = true }
|
||||||
click = {version = ">=8.1.2", optional = true}
|
click = {version = ">=8.1.2", optional = true}
|
||||||
msgpack = { version = ">=1.0.3", optional = true }
|
msgpack = { version = ">=1.0.3", optional = true }
|
||||||
mkdocs-exclude = "^1.0.2"
|
mkdocs-exclude = { version = "^1.0.2", optional = true }
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
dev = ["aiohttp", "click", "msgpack"]
|
dev = ["aiohttp", "click", "msgpack"]
|
||||||
|
|||||||
@@ -2175,7 +2175,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
for child in widget._nodes:
|
for child in widget._nodes:
|
||||||
push(child)
|
push(child)
|
||||||
|
|
||||||
def _remove_nodes(self, widgets: list[Widget]) -> AwaitRemove:
|
def _remove_nodes(self, widgets: list[Widget], parent: DOMNode) -> AwaitRemove:
|
||||||
"""Remove nodes from DOM, and return an awaitable that awaits cleanup.
|
"""Remove nodes from DOM, and return an awaitable that awaits cleanup.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -2198,7 +2198,8 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
await self._prune_nodes(widgets)
|
await self._prune_nodes(widgets)
|
||||||
finally:
|
finally:
|
||||||
finished_event.set()
|
finished_event.set()
|
||||||
self.refresh(layout=True)
|
if parent.styles.auto_dimensions:
|
||||||
|
parent.refresh(layout=True)
|
||||||
|
|
||||||
removed_widgets = self._detach_from_dom(widgets)
|
removed_widgets = self._detach_from_dom(widgets)
|
||||||
|
|
||||||
|
|||||||
@@ -551,6 +551,52 @@ class Color(NamedTuple):
|
|||||||
return (WHITE if white_contrast > black_contrast else BLACK).with_alpha(alpha)
|
return (WHITE if white_contrast > black_contrast else BLACK).with_alpha(alpha)
|
||||||
|
|
||||||
|
|
||||||
|
class Gradient:
|
||||||
|
"""Defines a color gradient."""
|
||||||
|
|
||||||
|
def __init__(self, *stops: tuple[float, Color]) -> None:
|
||||||
|
"""Create a color gradient that blends colors to form a spectrum.
|
||||||
|
|
||||||
|
A gradient is defined by a sequence of "stops" consisting of a float and a color.
|
||||||
|
The stop indicate the color at that point on a spectrum between 0 and 1.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stops: A colors stop.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If any stops are missing (must be at least a stop for 0 and 1).
|
||||||
|
"""
|
||||||
|
self.stops = sorted(stops)
|
||||||
|
if len(stops) < 2:
|
||||||
|
raise ValueError("At least 2 stops required.")
|
||||||
|
if self.stops[0][0] != 0.0:
|
||||||
|
raise ValueError("First stop must be 0.")
|
||||||
|
if self.stops[-1][0] != 1.0:
|
||||||
|
raise ValueError("Last stop must be 1.")
|
||||||
|
|
||||||
|
def get_color(self, position: float) -> Color:
|
||||||
|
"""Get a color from the gradient at a position between 0 and 1.
|
||||||
|
|
||||||
|
Positions that are between stops will return a blended color.
|
||||||
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
factor: A number between 0 and 1, where 0 is the first stop, and 1 is the last.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A color.
|
||||||
|
"""
|
||||||
|
# TODO: consider caching
|
||||||
|
position = clamp(position, 0.0, 1.0)
|
||||||
|
for (stop1, color1), (stop2, color2) in zip(self.stops, self.stops[1:]):
|
||||||
|
if stop2 >= position >= stop1:
|
||||||
|
return color1.blend(
|
||||||
|
color2,
|
||||||
|
(position - stop1) / (stop2 - stop1),
|
||||||
|
)
|
||||||
|
return self.stops[-1][1]
|
||||||
|
|
||||||
|
|
||||||
# Color constants
|
# Color constants
|
||||||
WHITE = Color(255, 255, 255)
|
WHITE = Color(255, 255, 255)
|
||||||
BLACK = Color(0, 0, 0)
|
BLACK = Color(0, 0, 0)
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ class DOMQuery(Generic[QueryType]):
|
|||||||
An awaitable object that waits for the widgets to be removed.
|
An awaitable object that waits for the widgets to be removed.
|
||||||
"""
|
"""
|
||||||
app = active_app.get()
|
app = active_app.get()
|
||||||
await_remove = app._remove_nodes(list(self))
|
await_remove = app._remove_nodes(list(self), self._node)
|
||||||
return await_remove
|
return await_remove
|
||||||
|
|
||||||
def set_styles(
|
def set_styles(
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ class DOMNode(MessagePump):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_refresh(self) -> float | None:
|
def auto_refresh(self) -> float | None:
|
||||||
|
"""Interval to refresh widget, or `None` for no automatic refresh."""
|
||||||
return self._auto_refresh
|
return self._auto_refresh
|
||||||
|
|
||||||
@auto_refresh.setter
|
@auto_refresh.setter
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class Driver(ABC):
|
|||||||
and not event.button
|
and not event.button
|
||||||
and self._last_move_event is not None
|
and self._last_move_event is not None
|
||||||
):
|
):
|
||||||
|
# Deduplicate self._down_buttons while preserving order.
|
||||||
buttons = list(dict.fromkeys(self._down_buttons).keys())
|
buttons = list(dict.fromkeys(self._down_buttons).keys())
|
||||||
self._down_buttons.clear()
|
self._down_buttons.clear()
|
||||||
move_event = self._last_move_event
|
move_event = self._last_move_event
|
||||||
|
|||||||
@@ -526,6 +526,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
await self.on_event(message)
|
await self.on_event(message)
|
||||||
else:
|
else:
|
||||||
await self._on_message(message)
|
await self._on_message(message)
|
||||||
|
if self._next_callbacks:
|
||||||
await self._flush_next_callbacks()
|
await self._flush_next_callbacks()
|
||||||
|
|
||||||
def _get_dispatch_methods(
|
def _get_dispatch_methods(
|
||||||
|
|||||||
@@ -1010,11 +1010,11 @@ class Widget(DOMNode):
|
|||||||
show_vertical = self.virtual_size.height > height
|
show_vertical = self.virtual_size.height > height
|
||||||
|
|
||||||
# When a single scrollbar is shown, the other dimension changes, so we need to recalculate.
|
# When a single scrollbar is shown, the other dimension changes, so we need to recalculate.
|
||||||
if show_vertical and not show_horizontal:
|
if overflow_x == "auto" and show_vertical and not show_horizontal:
|
||||||
show_horizontal = self.virtual_size.width > (
|
show_horizontal = self.virtual_size.width > (
|
||||||
width - styles.scrollbar_size_vertical
|
width - styles.scrollbar_size_vertical
|
||||||
)
|
)
|
||||||
if show_horizontal and not show_vertical:
|
if overflow_y == "auto" and show_horizontal and not show_vertical:
|
||||||
show_vertical = self.virtual_size.height > (
|
show_vertical = self.virtual_size.height > (
|
||||||
height - styles.scrollbar_size_horizontal
|
height - styles.scrollbar_size_horizontal
|
||||||
)
|
)
|
||||||
@@ -2548,7 +2548,7 @@ class Widget(DOMNode):
|
|||||||
An awaitable object that waits for the widget to be removed.
|
An awaitable object that waits for the widget to be removed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await_remove = self.app._remove_nodes([self])
|
await_remove = self.app._remove_nodes([self], self.parent)
|
||||||
return await_remove
|
return await_remove
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ if typing.TYPE_CHECKING:
|
|||||||
from ._label import Label
|
from ._label import Label
|
||||||
from ._list_item import ListItem
|
from ._list_item import ListItem
|
||||||
from ._list_view import ListView
|
from ._list_view import ListView
|
||||||
|
from ._loading_indicator import LoadingIndicator
|
||||||
from ._markdown import Markdown, MarkdownViewer
|
from ._markdown import Markdown, MarkdownViewer
|
||||||
from ._placeholder import Placeholder
|
from ._placeholder import Placeholder
|
||||||
from ._pretty import Pretty
|
from ._pretty import Pretty
|
||||||
@@ -45,6 +46,7 @@ __all__ = [
|
|||||||
"Label",
|
"Label",
|
||||||
"ListItem",
|
"ListItem",
|
||||||
"ListView",
|
"ListView",
|
||||||
|
"LoadingIndicator",
|
||||||
"Markdown",
|
"Markdown",
|
||||||
"MarkdownViewer",
|
"MarkdownViewer",
|
||||||
"Placeholder",
|
"Placeholder",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from ._input import Input as Input
|
|||||||
from ._label import Label as Label
|
from ._label import Label as Label
|
||||||
from ._list_item import ListItem as ListItem
|
from ._list_item import ListItem as ListItem
|
||||||
from ._list_view import ListView as ListView
|
from ._list_view import ListView as ListView
|
||||||
|
from ._loading_indicator import LoadingIndicator as LoadingIndicator
|
||||||
from ._markdown import Markdown as Markdown
|
from ._markdown import Markdown as Markdown
|
||||||
from ._markdown import MarkdownViewer as MarkdownViewer
|
from ._markdown import MarkdownViewer as MarkdownViewer
|
||||||
from ._placeholder import Placeholder as Placeholder
|
from ._placeholder import Placeholder as Placeholder
|
||||||
|
|||||||
63
src/textual/widgets/_loading_indicator.py
Normal file
63
src/textual/widgets/_loading_indicator.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from rich.console import RenderableType
|
||||||
|
from rich.style import Style
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from ..color import Gradient
|
||||||
|
from ..widget import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class LoadingIndicator(Widget):
|
||||||
|
"""Display an animated loading indicator."""
|
||||||
|
|
||||||
|
COMPONENT_CLASSES = {"loading-indicator--dot"}
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
LoadingIndicator {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingIndicator > .loading-indicator--dot {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self._start_time = time()
|
||||||
|
self.auto_refresh = 1 / 16
|
||||||
|
|
||||||
|
def render(self) -> RenderableType:
|
||||||
|
elapsed = time() - self._start_time
|
||||||
|
speed = 0.8
|
||||||
|
dot = "\u25CF"
|
||||||
|
dot_styles = self.get_component_styles("loading-indicator--dot")
|
||||||
|
|
||||||
|
base_style = self.rich_style
|
||||||
|
background = self.background_colors[-1]
|
||||||
|
color = dot_styles.color
|
||||||
|
|
||||||
|
gradient = Gradient(
|
||||||
|
(0.0, background.blend(color, 0.1)),
|
||||||
|
(0.7, color),
|
||||||
|
(1.0, color.lighten(0.1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
blends = [(elapsed * speed - dot_number / 8) % 1 for dot_number in range(5)]
|
||||||
|
|
||||||
|
dots = [
|
||||||
|
(
|
||||||
|
f"{dot} ",
|
||||||
|
base_style
|
||||||
|
+ Style.from_color(gradient.get_color((1 - blend) ** 2).rich_color),
|
||||||
|
)
|
||||||
|
for blend in blends
|
||||||
|
]
|
||||||
|
indicator = Text.assemble(*dots)
|
||||||
|
indicator.rstrip()
|
||||||
|
return indicator
|
||||||
File diff suppressed because one or more lines are too long
189
tests/snapshot_tests/snapshot_apps/auto-table.py
Normal file
189
tests/snapshot_tests/snapshot_apps/auto-table.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
from os import urandom
|
||||||
|
from random import randrange
|
||||||
|
|
||||||
|
from textual.app import App
|
||||||
|
from textual.containers import Container, Horizontal, Vertical
|
||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import DataTable, Header, Label
|
||||||
|
|
||||||
|
|
||||||
|
class LabeledBox(Container):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
LabeledBox {
|
||||||
|
layers: base_ top_;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
LabeledBox > Container {
|
||||||
|
layer: base_;
|
||||||
|
border: round $primary;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
layout: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
LabeledBox > Label {
|
||||||
|
layer: top_;
|
||||||
|
offset-x: 2;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, title, *args, **kwargs):
|
||||||
|
self.__label = Label(title)
|
||||||
|
|
||||||
|
super().__init__(self.__label, Container(*args, **kwargs))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self):
|
||||||
|
return self.__label
|
||||||
|
|
||||||
|
|
||||||
|
class StatusTable(DataTable):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.cursor_type = "row"
|
||||||
|
self.show_cursor = False
|
||||||
|
self.add_column("Foo")
|
||||||
|
self.add_column("Bar")
|
||||||
|
self.add_column("Baz")
|
||||||
|
|
||||||
|
for _ in range(50):
|
||||||
|
self.add_row(
|
||||||
|
"ABCDEFGH",
|
||||||
|
"0123456789",
|
||||||
|
"IJKLMNOPQRSTUVWXYZ",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Status(LabeledBox):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
Status {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Container {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status StatusTable {
|
||||||
|
width: auto;
|
||||||
|
margin-top: 1;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
self.__name = name
|
||||||
|
self.__table = StatusTable()
|
||||||
|
|
||||||
|
super().__init__(f" {self.__name} ", self.__table)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def table(self) -> StatusTable:
|
||||||
|
return self.__table
|
||||||
|
|
||||||
|
|
||||||
|
class Rendering(LabeledBox):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
#issue-info {
|
||||||
|
height: auto;
|
||||||
|
border-bottom: dashed #632CA6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statuses-box {
|
||||||
|
height: 1fr;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__info = Label("test")
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
"",
|
||||||
|
Container(
|
||||||
|
Horizontal(self.__info, id="issue-info"),
|
||||||
|
Horizontal(*[Status(str(i)) for i in range(4)], id="statuses-box"),
|
||||||
|
id="issues-box",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> Label:
|
||||||
|
return self.__info
|
||||||
|
|
||||||
|
|
||||||
|
class Sidebar(LabeledBox):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
#sidebar-status {
|
||||||
|
height: auto;
|
||||||
|
border-bottom: dashed #632CA6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-options {
|
||||||
|
height: 1fr;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__status = Label("ok")
|
||||||
|
self.__options = Vertical()
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
"",
|
||||||
|
Container(self.__status, id="sidebar-status"),
|
||||||
|
Container(self.__options, id="sidebar-options"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> Label:
|
||||||
|
return self.__status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> Vertical:
|
||||||
|
return self.__options
|
||||||
|
|
||||||
|
|
||||||
|
class MyScreen(Screen):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
#main-content {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 2;
|
||||||
|
grid-columns: 1fr 5fr;
|
||||||
|
grid-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content-sidebar {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content-rendering {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
yield Header()
|
||||||
|
yield Container(
|
||||||
|
Container(Sidebar(), id="main-content-sidebar"),
|
||||||
|
Container(Rendering(), id="main-content-rendering"),
|
||||||
|
id="main-content",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MyApp(App):
|
||||||
|
async def on_mount(self):
|
||||||
|
self.install_screen(MyScreen(), "myscreen")
|
||||||
|
await self.push_screen("myscreen")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = MyApp()
|
||||||
|
app.run()
|
||||||
38
tests/snapshot_tests/snapshot_apps/remove_auto.py
Normal file
38
tests/snapshot_tests/snapshot_apps/remove_auto.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Vertical
|
||||||
|
from textual.widgets import Header, Footer, Label
|
||||||
|
|
||||||
|
|
||||||
|
class VerticalRemoveApp(App[None]):
|
||||||
|
CSS = """
|
||||||
|
Vertical {
|
||||||
|
border: round green;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
border: round yellow;
|
||||||
|
background: red;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
BINDINGS = [
|
||||||
|
("a", "add", "Add"),
|
||||||
|
("d", "del", "Delete"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Vertical()
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def action_add(self) -> None:
|
||||||
|
self.query_one(Vertical).mount(Label("This is a test label"))
|
||||||
|
|
||||||
|
def action_del(self) -> None:
|
||||||
|
if self.query_one(Vertical).children:
|
||||||
|
self.query_one(Vertical).children[-1].remove()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
VerticalRemoveApp().run()
|
||||||
@@ -133,12 +133,12 @@ def test_header_render(snap_compare):
|
|||||||
|
|
||||||
def test_list_view(snap_compare):
|
def test_list_view(snap_compare):
|
||||||
assert snap_compare(
|
assert snap_compare(
|
||||||
WIDGET_EXAMPLES_DIR / "list_view.py", press=["tab", "down", "down", "up", "_"]
|
WIDGET_EXAMPLES_DIR / "list_view.py", press=["tab", "down", "down", "up"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_textlog_max_lines(snap_compare):
|
def test_textlog_max_lines(snap_compare):
|
||||||
assert snap_compare("snapshot_apps/textlog_max_lines.py", press=[*"abcde", "_"])
|
assert snap_compare("snapshot_apps/textlog_max_lines.py", press=[*"abcde"])
|
||||||
|
|
||||||
|
|
||||||
def test_fr_units(snap_compare):
|
def test_fr_units(snap_compare):
|
||||||
@@ -213,7 +213,7 @@ def test_order_independence(snap_compare):
|
|||||||
|
|
||||||
|
|
||||||
def test_order_independence_toggle(snap_compare):
|
def test_order_independence_toggle(snap_compare):
|
||||||
assert snap_compare("snapshot_apps/layer_order_independence.py", press="t,_")
|
assert snap_compare("snapshot_apps/layer_order_independence.py", press="t")
|
||||||
|
|
||||||
|
|
||||||
def test_columns_height(snap_compare):
|
def test_columns_height(snap_compare):
|
||||||
@@ -228,7 +228,7 @@ def test_offsets(snap_compare):
|
|||||||
|
|
||||||
def test_nested_auto_heights(snap_compare):
|
def test_nested_auto_heights(snap_compare):
|
||||||
"""Test refreshing widget within a auto sized container"""
|
"""Test refreshing widget within a auto sized container"""
|
||||||
assert snap_compare("snapshot_apps/nested_auto_heights.py", press=["1", "2", "_"])
|
assert snap_compare("snapshot_apps/nested_auto_heights.py", press=["1", "2"])
|
||||||
|
|
||||||
|
|
||||||
def test_programmatic_scrollbar_gutter_change(snap_compare):
|
def test_programmatic_scrollbar_gutter_change(snap_compare):
|
||||||
@@ -306,4 +306,14 @@ def test_focus_component_class(snap_compare):
|
|||||||
|
|
||||||
|
|
||||||
def test_line_api_scrollbars(snap_compare):
|
def test_line_api_scrollbars(snap_compare):
|
||||||
assert snap_compare(SNAPSHOT_APPS_DIR / "line_api_scrollbars.py", press=["_"])
|
assert snap_compare(SNAPSHOT_APPS_DIR / "line_api_scrollbars.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_with_auto_height(snap_compare):
|
||||||
|
assert snap_compare(
|
||||||
|
SNAPSHOT_APPS_DIR / "remove_auto.py", press=["a", "a", "a", "d", "d"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_table(snap_compare):
|
||||||
|
assert snap_compare(SNAPSHOT_APPS_DIR / "auto-table.py", terminal_size=(120, 40))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import pytest
|
|||||||
from rich.color import Color as RichColor
|
from rich.color import Color as RichColor
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from textual.color import Color, Lab, lab_to_rgb, rgb_to_lab
|
from textual.color import Color, Gradient, Lab, lab_to_rgb, rgb_to_lab
|
||||||
|
|
||||||
|
|
||||||
def test_rich_color():
|
def test_rich_color():
|
||||||
@@ -215,3 +215,31 @@ def test_rgb_lab_rgb_roundtrip():
|
|||||||
|
|
||||||
def test_inverse():
|
def test_inverse():
|
||||||
assert Color(55, 0, 255, 0.1).inverse == Color(200, 255, 0, 0.1)
|
assert Color(55, 0, 255, 0.1).inverse == Color(200, 255, 0, 0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gradient_errors():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Gradient()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Gradient((0, Color.parse("red")))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Gradient(
|
||||||
|
(0, Color.parse("red")),
|
||||||
|
(0.8, Color.parse("blue")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gradient():
|
||||||
|
gradient = Gradient(
|
||||||
|
(0, Color(255, 0, 0)),
|
||||||
|
(0.5, Color(0, 0, 255)),
|
||||||
|
(1, Color(0, 255, 0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert gradient.get_color(-1) == Color(255, 0, 0)
|
||||||
|
assert gradient.get_color(0) == Color(255, 0, 0)
|
||||||
|
assert gradient.get_color(1) == Color(0, 255, 0)
|
||||||
|
assert gradient.get_color(1.2) == Color(0, 255, 0)
|
||||||
|
assert gradient.get_color(0.5) == Color(0, 0, 255)
|
||||||
|
assert gradient.get_color(0.7) == Color(0, 101, 153)
|
||||||
|
|||||||
Reference in New Issue
Block a user