Merge branch 'css' of github.com:Textualize/textual into docs-animator

This commit is contained in:
Darren Burns
2022-10-06 14:11:19 +01:00
57 changed files with 919 additions and 168 deletions

View File

@@ -8,7 +8,7 @@ The `Click` event is sent to a widget when the user clicks a mouse button.
## Attributes
| attribute | type | purpose |
| ---------- | ---- | ----------------------------------------- |
|------------|------|-------------------------------------------|
| `x` | int | Mouse x coordinate, relative to Widget |
| `y` | int | Mouse y coordinate, relative to Widget |
| `delta_x` | int | Change in x since last mouse event |

View File

@@ -8,7 +8,7 @@ The `MouseMove` event is sent to a widget when the mouse pointer is moved over a
## Attributes
| attribute | type | purpose |
| ---------- | ---- | ----------------------------------------- |
|------------|------|-------------------------------------------|
| `x` | int | Mouse x coordinate, relative to Widget |
| `y` | int | Mouse y coordinate, relative to Widget |
| `delta_x` | int | Change in x since last mouse event |

View File

@@ -8,7 +8,7 @@ The `MouseRelease` event is sent to a widget when it is no longer receiving mous
## Attributes
| attribute | type | purpose |
| ---------------- | ------ | --------------------------------------------- |
|------------------|--------|-----------------------------------------------|
| `mouse_position` | Offset | Mouse coordinates when the mouse was released |
## Code

View File

@@ -8,7 +8,7 @@ The `MouseScrollDown` event is sent to a widget when the scroll wheel (or trackp
## Attributes
| attribute | type | purpose |
| --------- | ---- | -------------------------------------- |
|-----------|------|----------------------------------------|
| `x` | int | Mouse x coordinate, relative to Widget |
| `y` | int | Mouse y coordinate, relative to Widget |

View File

@@ -8,7 +8,7 @@ The `MouseScrollUp` event is sent to a widget when the scroll wheel (or trackpad
## Attributes
| attribute | type | purpose |
| --------- | ---- | -------------------------------------- |
|-----------|------|----------------------------------------|
| `x` | int | Mouse x coordinate, relative to Widget |
| `y` | int | Mouse y coordinate, relative to Widget |

View File

@@ -8,7 +8,7 @@ The `MouseUp` event is sent to a widget when the user releases a mouse button.
## Attributes
| attribute | type | purpose |
| ---------- | ---- | ----------------------------------------- |
|------------|------|-------------------------------------------|
| `x` | int | Mouse x coordinate, relative to Widget |
| `y` | int | Mouse y coordinate, relative to Widget |
| `delta_x` | int | Change in x since last mouse event |

View File

@@ -8,7 +8,7 @@ The `Paste` event is sent to a widget when the user pastes text.
## Attributes
| attribute | type | purpose |
| --------- | ---- | ------------------------ |
|-----------|------|--------------------------|
| `text` | str | The text that was pasted |
## Code

View File

@@ -8,7 +8,7 @@ The `Resize` event is sent to a widget when its size changes and when it is firs
## Attributes
| attribute | type | purpose |
| ---------------- | ---- | ------------------------------------------------ |
|------------------|------|--------------------------------------------------|
| `size` | Size | The new size of the Widget |
| `virtual_size` | Size | The virtual size (scrollable area) of the Widget |
| `container_size` | Size | The size of the container (parent widget) |

View File

@@ -0,0 +1,16 @@
#dialog {
grid-size: 2;
grid-gutter: 1 2;
margin: 1 2;
}
#question {
column-span: 2;
content-align: center bottom;
width: 100%;
height: 100%;
}
Button {
width: 100%;
}

View File

@@ -0,0 +1,37 @@
from textual.app import App, ComposeResult
from textual.containers import Grid
from textual.screen import Screen
from textual.widgets import Static, Header, Footer, Button
class QuitScreen(Screen):
def compose(self) -> ComposeResult:
yield Grid(
Static("Are you sure you want to quit?", id="question"),
Button("Quit", variant="error", id="quit"),
Button("Cancel", variant="primary", id="cancel"),
id="dialog",
)
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "quit":
self.app.exit()
else:
self.app.pop_screen()
class ModalApp(App):
CSS_PATH = "modal01.css"
BINDINGS = [("q", "request_quit", "Quit")]
def compose(self) -> ComposeResult:
yield Header()
yield Footer()
def action_request_quit(self) -> None:
self.push_screen(QuitScreen())
if __name__ == "__main__":
app = ModalApp()
app.run()

View File

@@ -0,0 +1,18 @@
BSOD {
align: center middle;
background: blue;
color: white;
}
BSOD>Static {
width: 70;
}
#title {
content-align-horizontal: center;
text-style: reverse;
}
#any-key {
content-align-horizontal: center;
}

View File

@@ -0,0 +1,34 @@
from textual.app import App, Screen, ComposeResult
from textual.widgets import Static
ERROR_TEXT = """
An error has occurred. To continue:
Press Enter to return to Windows, or
Press CTRL+ALT+DEL to restart your computer. If you do this,
you will lose any unsaved information in all open applications.
Error: 0E : 016F : BFF9B3D4
"""
class BSOD(Screen):
BINDINGS = [("escape", "app.pop_screen", "Pop screen")]
def compose(self) -> ComposeResult:
yield Static(" Windows ", id="title")
yield Static(ERROR_TEXT)
yield Static("Press any key to continue [blink]_[/]", id="any-key")
class BSODApp(App):
CSS_PATH = "screen01.css"
SCREENS = {"bsod": BSOD()}
BINDINGS = [("b", "push_screen('bsod')", "BSOD")]
if __name__ == "__main__":
app = BSODApp()
app.run()

View File

@@ -0,0 +1,18 @@
BSOD {
align: center middle;
background: blue;
color: white;
}
BSOD>Static {
width: 70;
}
#title {
content-align-horizontal: center;
text-style: reverse;
}
#any-key {
content-align-horizontal: center;
}

View File

@@ -0,0 +1,36 @@
from textual.app import App, Screen, ComposeResult
from textual.widgets import Static
ERROR_TEXT = """
An error has occurred. To continue:
Press Enter to return to Windows, or
Press CTRL+ALT+DEL to restart your computer. If you do this,
you will lose any unsaved information in all open applications.
Error: 0E : 016F : BFF9B3D4
"""
class BSOD(Screen):
BINDINGS = [("escape", "app.pop_screen", "Pop screen")]
def compose(self) -> ComposeResult:
yield Static(" Windows ", id="title")
yield Static(ERROR_TEXT)
yield Static("Press any key to continue [blink]_[/]", id="any-key")
class BSODApp(App):
CSS_PATH = "screen02.css"
BINDINGS = [("b", "push_screen('bsod')", "BSOD")]
def on_mount(self) -> None:
self.install_screen(BSOD(), name="bsod")
if __name__ == "__main__":
app = BSODApp()
app.run()

View File

@@ -0,0 +1,15 @@
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Footer
class FooterApp(App):
BINDINGS = [Binding(key="q", action="quit", description="Quit the app")]
def compose(self) -> ComposeResult:
yield Footer()
if __name__ == "__main__":
app = FooterApp()
app.run()

View File

@@ -0,0 +1,12 @@
from textual.app import App, ComposeResult
from textual.widgets import Header
class HeaderApp(App):
def compose(self) -> ComposeResult:
yield Header()
if __name__ == "__main__":
app = HeaderApp()
app.run()

View File

@@ -0,0 +1,13 @@
from textual.app import App, ComposeResult
from textual.widgets import Input
class InputApp(App):
def compose(self) -> ComposeResult:
yield Input(placeholder="First Name")
yield Input(placeholder="Last Name")
if __name__ == "__main__":
app = InputApp()
app.run()

View File

@@ -0,0 +1,12 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
class StaticApp(App):
def compose(self) -> ComposeResult:
yield Static("Hello, world!")
if __name__ == "__main__":
app = StaticApp()
app.run()

View File

@@ -0,0 +1,29 @@
import csv
import io
from textual.app import App, ComposeResult
from textual.widgets import DataTable
CSV = """lane,swimmer,country,time
4,Joseph Schooling,Singapore,50.39
2,Michael Phelps,United States,51.14
5,Chad le Clos,South Africa,51.14
6,László Cseh,Hungary,51.14
3,Li Zhuhao,China,51.26
8,Mehdy Metella,France,51.58
7,Tom Shields,United States,51.73
1,Aleksandr Sadovnikov,Russia,51.84"""
class TableApp(App):
def compose(self) -> ComposeResult:
yield DataTable()
def on_mount(self) -> None:
table = self.query_one(DataTable)
rows = csv.reader(io.StringIO(CSV))
table.add_columns(*next(rows))
table.add_rows(rows)
app = TableApp()

View File

@@ -24,19 +24,23 @@ You can install Textual via PyPI.
If you plan on developing Textual apps, then you should install `textual[dev]`. The `[dev]` part installs a few extra dependencies for development.
```bash
pip install "textual[dev]"
```
pip install "textual[dev]==0.2.0b5"
```
If you only plan on _running_ Textual apps, then you can drop the `[dev]` part:
```bash
pip install textual
```
pip install textual==0.2.0b5
```
!!! important
There may be a more recent beta version since the time of writing. Check the [release history](https://pypi.org/project/textual/#history) for a more recent version.
## Textual CLI
If you installed the dev dependencies you have have access to the `textual` CLI command. There are a number of sub-commands which will aid you in building Textual apps.
If you installed the dev dependencies you have access to the `textual` CLI command. There are a number of sub-commands which will aid you in building Textual apps.
```bash
textual --help

View File

@@ -131,10 +131,26 @@ Textual supports the following builtin actions which are defined on the app.
options:
show_root_heading: false
### Push screen
::: textual.app.App.action_push_screen
### Pop screen
::: textual.app.App.action_pop_screen
### Screenshot
::: textual.app.App.action_screenshot
### Switch screen
::: textual.app.App.action_switch_screen
### Toggle_dark
::: textual.app.App.action_toggle_dark

View File

@@ -1,12 +1,151 @@
# Screens
TODO: Screens docs
This chapter covers Textual's screen API. We will discuss how to create screens and switch between them.
- Explanation of screens
- Screens API
- Install screen
- Uninstall screen
- Push screen
- Pop screen
- Switch Screen
- Screens example
## What is a screen?
Screens are containers for widgets that occupy the dimensions of your terminal. There can be many screens in a given app, but only one screen is visible at a time.
Textual requires that there be at least one screen object and will create one implicitly in the App class. If you don't change the screen, any widgets you [mount][textual.widget.Widget.mount] or [compose][textual.widget.Widget.compose] will be added to this default screen.
!!! tip
Try printing `widget.parent` to see what object your widget is connected to.
<div class="excalidraw">
--8<-- "docs/images/dom1.excalidraw.svg"
</div>
## Creating a screen
You can create a screen by extending the [Screen][textual.screen.Screen] class which you can import from `textual.screen`. The screen may be styled in the same way as other widgets, with the exception that you can't modify the screen's dimensions (as these will always be the size of your terminal).
Let's look at a simple example of writing a screen class to simulate Window's [blue screen of death](https://en.wikipedia.org/wiki/Blue_screen_of_death).
=== "screen01.py"
```python title="screen01.py" hl_lines="17-23 28"
--8<-- "docs/examples/guide/screens/screen01.py"
```
=== "screen01.css"
```sass title="screen01.css"
--8<-- "docs/examples/guide/screens/screen01.css"
```
=== "Output"
```{.textual path="docs/examples/guide/screens/screen01.py" press="b,_"}
```
If you run this you will see an empty screen. Hit the ++b++ screen to show a blue screen of death. Hit ++escape++ to return to the default screen.
The `BSOD` class above defines a screen with a key binding and compose method. These should be familiar as they work in the same way as apps.
The app class has a new `SCREENS` class variable. Textual uses this class variable to associated a name with screen object (the name is used to reference screens in the screen API). Also in the app is a key binding associated with the action `"push_screen('bsod')"`. The screen class has a similar action `"pop_screen"` bound to the ++escape++ key. We will cover these actions below.
## Named screens
You can associate a screen with a name by defining a `SCREENS` class variable in your app, which should be dict that maps names on to Screen objects. The name of the screen may be used interchangeably with screen objects in much of the screen API.
You can also _install_ new named screens dynamically with the [install_screen][textual.app.App.install_screen] method. The following example installs the `BSOD` screen in a mount handler rather than from the `SCREENS` variable.
=== "screen02.py"
```python title="screen02.py" hl_lines="30-31"
--8<-- "docs/examples/guide/screens/screen02.py"
```
=== "screen02.css"
```sass title="screen02.css"
--8<-- "docs/examples/guide/screens/screen02.css"
```
=== "Output"
```{.textual path="docs/examples/guide/screens/screen02.py" press="b,_"}
```
Although both do the same thing, we recommend the `SCREENS` for screens that exist for the lifetime of your app.
### Uninstalling screens
Screens defined in `SCREENS` or added with [install_screen][textual.app.App.install_screen] are _installed_ screens. Textual will keep these screens in memory for the lifetime of your app.
If you have installed a screen, but you later want it to be removed and cleaned up, you can call [uninstall_screen][textual.app.App.uninstall_screen].
## Screen stack
Textual keeps track of a _stack_ of screens. You can think of the screen stack as a stack of paper, where only the very top sheet is visible. If you remove the top sheet the paper underneath becomes visible. Screens work in a similar way.
The active screen (top of the stack) will render the screen and receive input events. The following API methods on the App class can manipulate this stack, and let you decide which screen the user can interact with.
### Push screen
The [push_screen][textual.app.App.push_screen] method puts a screen on top of the stack and makes that screen active. You can call this method with the name of an installed screen, or a screen object.
<div class="excalidraw">
--8<-- "docs/images/screens/push_screen.excalidraw.svg"
</div>
#### Action
You can also push screens with the `"app.push_screen"` action, which requires the name of an installed screen.
### Pop screen
The [pop_screen][textual.app.App.pop_screen] method removes the top-most screen from the stack, and makes the new top screen active.
!!! note
The screen stack must always have at least one screen. If you attempt to remove the last screen, Textual will raise a [ScreenStackError][textual.app.ScreenStackError] exception.
<div class="excalidraw">
--8<-- "docs/images/screens/pop_screen.excalidraw.svg"
</div>
When you pop a screen it will be removed and deleted unless it has been installed or there is another copy of the screen on the stack.
#### Action
You can also pop screens with the `"app.pop_screen"` action.
### Switch screen
The [switch_screen][textual.app.App.switch_screen] method replaces the top of the stack with a new screen.
<div class="excalidraw">
--8<-- "docs/images/screens/switch_screen.excalidraw.svg"
</div>
Like [pop_screen](#pop-screen), if the screen being replaced is not installed it will be removed and deleted.
#### Action
You can also switch screens with the `"app.switch_screen"` action which accepts the name of the screen to switch to.
## Modal screens
Screens can be used to implement modal dialogs. The following example pushes a screen when you hit the ++q++ key to ask you if you really want to quit.
=== "modal01.py"
```python title="modal01.py" hl_lines="18 20 32"
--8<-- "docs/examples/guide/screens/modal01.py"
```
=== "modal01.css"
```sass title="modal01.css"
--8<-- "docs/examples/guide/screens/modal01.css"
```
=== "Output"
```{.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.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1 +1 @@
::: textual.app.App
::: textual.app

View File

@@ -0,0 +1 @@
::: textual.widgets.DataTable

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

@@ -0,0 +1 @@
::: textual.widgets.Footer

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

@@ -0,0 +1 @@
::: textual.widgets.Header

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

@@ -0,0 +1 @@
::: textual.widgets.Input

View File

@@ -35,13 +35,13 @@ If you want to try the finished Stopwatch app and follow along with the code, fi
=== "HTTPS"
```bash
git clone https://github.com/Textualize/textual.git
git clone -b css https://github.com/Textualize/textual.git
```
=== "SSH"
```bash
git clone git@github.com:Textualize/textual.git
git clone -b css git@github.com:Textualize/textual.git
```
=== "GitHub CLI"
@@ -50,6 +50,7 @@ If you want to try the finished Stopwatch app and follow along with the code, fi
gh repo clone Textualize/textual
```
With the repository cloned, navigate to `docs/examples/tutorial` and run `stopwatch.py`.
```bash
@@ -152,7 +153,7 @@ Textual has a builtin `Button` widget which takes care of the first three compon
Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go.
```python title="stopwatch02.py" hl_lines="3 6-7 10-18 30"
```python title="stopwatch02.py" hl_lines="2-3 6-7 10-18 30"
--8<-- "docs/examples/tutorial/stopwatch02.py"
```
@@ -160,7 +161,7 @@ We've imported two new widgets in this code: `Button`, which creates a clickable
We've defined an empty `TimeDisplay` widget by extending `Static`. We will flesh this out later.
The Stopwatch widget also class extends `Static`. This class has a `compose()` method which yields child widgets, consisting of three `Button` objects and a single `TimeDisplay`. These widgets will form the stopwatch in our sketch.
The Stopwatch widget class also extends `Static`. This class has a `compose()` method which yields child widgets, consisting of three `Button` objects and a single `TimeDisplay` object. These widgets will form the stopwatch in our sketch.
#### The buttons
@@ -377,7 +378,7 @@ We've seen how we can update widgets with a timer, but we still need to wire up
We need to be able to start, stop, and reset each stopwatch independently. We can do this by adding a few more methods to the `TimeDisplay` class.
```python title="stopwatch06.py" hl_lines="14 30-44 50-61"
```python title="stopwatch06.py" hl_lines="14 18 30-44 50-61"
--8<-- "docs/examples/tutorial/stopwatch06.py"
```
@@ -431,6 +432,7 @@ Let's use these methods to implement adding and removing stopwatches to our app.
Here's a summary of the changes:
- The Container object in StopWatchApp grew a "timers" ID.
- Added `action_add_stopwatch` to add a new stopwatch.
- Added `action_remove_stopwatch` to remove a stopwatch.
- Added keybindings for the actions.

View File

@@ -1,6 +1,5 @@
# Button
## Description
A simple button widget which can be pressed using a mouse click or by pressing ++return++
when it has focus.
@@ -33,16 +32,16 @@ Clicking any of the non-disabled buttons in the example app below will result in
## Reactive Attributes
| Name | Type | Default | Description |
|------------|--------|-------------|-----------------------------------------------------------------------------------------------------------------------------------|
| ---------- | ------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `label` | `str` | `""` | The text that appears inside the button. |
| `variant` | `str` | `"default"` | Semantic styling variant. One of `default`, `primary`, `success`, `warning`, `error`. |
| `disabled` | `bool` | `False` | Whether the button is disabled or not. Disabled buttons cannot be focused or clicked, and are styled in a way that suggests this. |
## Events
## Messages
### Pressed
The `Button.Pressed` event is sent when the button is pressed.
The `Button.Pressed` message is sent when the button is pressed.
- [x] Bubbles

View File

@@ -1 +1,38 @@
# DataTable
A data table widget.
- [x] Focusable
- [ ] Container
## Example
The example below populates a table with CSV data.
=== "Output"
```{.textual path="docs/examples/widgets/table.py"}
```
=== "table.py"
```python
--8<-- "docs/examples/widgets/table.py"
```
## Reactive Attributes
| Name | Type | Default | Description |
| --------------- | ------ | ------- | ---------------------------------- |
| `show_header` | `bool` | `True` | Show the table header |
| `fixed_rows` | `int` | `0` | Number of fixed rows |
| `fixed_columns` | `int` | `0` | Number of fixed columns |
| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |
| `header_height` | `int` | `1` | Height of header row |
| `show_cursor` | `bool` | `True` | Show a cell cursor |
## See Also
* [Table][textual.widgets.DataTable] code reference

View File

@@ -1 +1,42 @@
# Footer
A simple footer widget which is docked to the bottom of its parent container. Displays
available keybindings for the currently focused widget.
- [ ] Focusable
- [ ] Container
## Example
The example below shows an app with a single keybinding that contains only a `Footer`
widget. Notice how the `Footer` automatically displays the keybind.
=== "Output"
```{.textual path="docs/examples/widgets/footer.py"}
```
=== "footer.py"
```python
--8<-- "docs/examples/widgets/footer.py"
```
## Reactive Attributes
| Name | Type | Default | Description |
| --------------- | ----- | ------- | --------------------------------------------------------------------------------------------------------- |
| `highlight_key` | `str` | `None` | Stores the currently highlighted key. This is typically the key the cursor is hovered over in the footer. |
## Messages
This widget sends no messages.
## Additional Notes
* You can prevent keybindings from appearing in the footer by setting the `show` argument of the `Binding` to `False`.
* You can customize the text that appears for the key itself in the footer using the `key_display` argument of `Binding`.
## See Also
* [Footer](../reference/footer.md) code reference

View File

@@ -1 +1,35 @@
# Header
A simple header widget which docks itself to the top of the parent container.
- [ ] Focusable
- [ ] Container
## Example
The example below shows an app with a `Header`.
=== "Output"
```{.textual path="docs/examples/widgets/header.py"}
```
=== "header.py"
```python
--8<-- "docs/examples/widgets/header.py"
```
## Reactive Attributes
| Name | Type | Default | Description |
| ------ | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tall` | `bool` | `True` | Whether the `Header` widget is displayed as tall or not. The tall variant is 3 cells tall by default. The non-tall variant is a single cell tall. This can be toggled by clicking on the header. |
## Messages
This widget sends no messages.
## See Also
* [Header](../reference/header.md) code reference

View File

@@ -1 +1,67 @@
# Input
A single-line text input widget.
- [x] Focusable
- [ ] Container
## Example
The example below shows how you might create a simple form using two `Input` widgets.
=== "Output"
```{.textual path="docs/examples/widgets/input.py" press="tab,D,a,r,r,e,n"}
```
=== "input.py"
```python
--8<-- "docs/examples/widgets/input.py"
```
## Reactive Attributes
| Name | Type | Default | Description |
| ----------------- | ------ | ------- | --------------------------------------------------------------- |
| `cursor_blink` | `bool` | `True` | True if cursor blinking is enabled. |
| `value` | `str` | `""` | The value currently in the text input. |
| `cursor_position` | `int` | `0` | The index of the cursor in the value string. |
| `placeholder` | `str` | `str` | The dimmed placeholder text to display when the input is empty. |
| `password` | `bool` | `False` | True if the input should be masked. |
## Messages
### Changed
The `Input.Changed` message is sent when the value in the text input changes.
- [x] Bubbles
#### Attributes
| attribute | type | purpose |
| --------- | ----- | -------------------------------- |
| `value` | `str` | The new value in the text input. |
### Submitted
The `Input.Submitted` message is sent when you press ++enter++ with the text field submitted.
- [x] Bubbles
#### Attributes
| attribute | type | purpose |
| --------- | ----- | -------------------------------- |
| `value` | `str` | The new value in the text input. |
## Additional Notes
* The spacing around the text content is due to border. To remove it, set `border: none;` in your CSS.
## See Also
* [Input](../reference/input.md) code reference

View File

@@ -1 +1,34 @@
# Static
A widget which displays static content.
Can be used for simple text labels, but can also contain more complex Rich renderables.
- [ ] Focusable
- [x] Container
## Example
The example below shows how you can use a `Static` widget as a simple text label.
=== "Output"
```{.textual path="docs/examples/widgets/static.py"}
```
=== "static.py"
```python
--8<-- "docs/examples/widgets/static.py"
```
## Reactive Attributes
This widget has no reactive attributes.
## Messages
This widget sends no messages.
## See Also
* [Static](../reference/static.md) code reference