Merge branch 'css' into mount-wait

This commit is contained in:
Will McGugan
2022-10-17 09:36:15 +01:00
25 changed files with 132 additions and 144 deletions

View File

@@ -17,7 +17,7 @@ Textual is a Python framework for creating interactive applications that run in
## About
Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with a Python API inspired by modern development development.
Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with a Python API inspired by modern web development.
On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience.

View File

@@ -1,4 +1,4 @@
# Show
# Hide
The `Hide` event is sent to a widget when it is hidden from view.

View File

@@ -105,7 +105,7 @@ With a header and a footer widget the DOM looks the this:
Both Header and Footer are children of the Screen object.
To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going import and use a few more builtin widgets:
To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going to import and use a few more builtin widgets:
- `textual.layout.Container` For our top-level dialog.
- `textual.layout.Horizontal` To arrange widgets left to right.
@@ -187,7 +187,7 @@ Let's look at the selectors supported by Textual CSS.
The _type_ selector matches the name of the (Python) class. For example, the following widget can be matched with a `Button` selector:
```python
from textual.widgets import Widget
from textual.widgets import Static
class Button(Static):
pass
@@ -218,7 +218,7 @@ You may have noticed that the `border` rule exists in both Static and Button. Wh
### ID selector
Every Widget can have a single `id` attribute, which is set via the constructor. The ID should be unique to it's container.
Every Widget can have a single `id` attribute, which is set via the constructor. The ID should be unique to its container.
Here's an example of a widget with an ID:
@@ -280,7 +280,7 @@ Unlike the `id` attribute, a widget's classes can be changed after the widget wa
- [add_class()][textual.dom.DOMNode.add_class] Adds one or more classes to a widget.
- [remove_class()][textual.dom.DOMNode.remove_class] Removes class name(s) from a widget.
- [toggle_class()][textual.dom.DOMNode.toggle_class] Removes a class name if it is present, or adds the name if it's not already present.
- [has_class()][textual.dom.DOMNode.has_class] Checks if a class(es) is set on a widget.
- [has_class()][textual.dom.DOMNode.has_class] Checks if one or more classes are set on a widget.
- [classes][textual.dom.DOMNode.classes] Is a frozen set of the class(es) set on a widget.
@@ -369,7 +369,7 @@ It is possible that several selectors match a given widget. If the same style is
- The selector with the most IDs wins. For instance `#next` beats `.button` and `#dialog #next` beats `#next`. If the selectors have the same number of IDs then move to the next rule.
- The selector with the most class names wins. For instance `.button.success` beats `.success`. For the purposes of specificity, pseudo classes are treated the same as regular class names, so ".button:hover" counts as _2_ class names. If the selectors have the same number of class names then move to the next rule.
- The selector with the most class names wins. For instance `.button.success` beats `.success`. For the purposes of specificity, pseudo classes are treated the same as regular class names, so `.button:hover` counts as _2_ class names. If the selectors have the same number of class names then move to the next rule.
- The selector with the most types wins. For instance `Container Button` beats `Button`.
@@ -427,4 +427,3 @@ Variables can refer to other variables.
Let's say we define a variable `$success: lime;`.
Our `$border` variable could then be updated to `$border: wide $success;`, which will
be translated to `$border: wide lime;`.

View File

@@ -50,7 +50,7 @@ Action strings have the following format:
### Parameters
If the action strings contains parameters, these must be valid Python literals. Which means you can include numbers, strings, dicts, lists etc. but you can't include variables or references to any other python symbols.
If the action string contains parameters, these must be valid Python literals. Which means you can include numbers, strings, dicts, lists etc. but you can't include variables or references to any other python symbol.
Consequently `"set_background('blue')"` is a valid action string, but `"set_background(new_color)"` is not — because `new_color` is a variable and not a literal.
@@ -71,7 +71,7 @@ The following example mounts simple static text with embedded action links.
```{.textual path="docs/examples/guide/actions/actions03.py"}
```
When you click any of the links, Textual runs the `"set_background"` action to change the background to the given color and plays the terminals bell.
When you click any of the links, Textual runs the `"set_background"` action to change the background to the given color and plays the terminal's bell.
## Bindings

View File

@@ -5,7 +5,7 @@ Ths chapter discusses how to use Textual's animation system to create visual eff
## Animating styles
Textual's animator can change an attribute from one value to another in fixed increments over a period of time. You can apply animations to [styles](styles.md) such `offset` to move widgets around the screen, and `opacity` to create fading effects.
Textual's animator can change an attribute from one value to another in fixed increments over a period of time. You can apply animations to [styles](styles.md) such as `offset` to move widgets around the screen, and `opacity` to create fading effects.
Apps and widgets both have an [animate][textual.app.App.animate] method which will animate properties on those objects. Additionally, `styles` objects have an identical `animate` method which will animate styles.
@@ -77,7 +77,7 @@ You can specify which easing method to use via the `easing` parameter on the `an
## Completion callbacks
You can pass an callable to the animator via the `on_complete` parameter. Textual will run the callable when the animation has completed.
You can pass a callable to the animator via the `on_complete` parameter. Textual will run the callable when the animation has completed.
## Delaying animations

View File

@@ -82,24 +82,24 @@ Textual knows to *await* your event handlers if they are coroutines (i.e. prefix
Widgets are self-contained components responsible for generating the output for a portion of the screen. Widgets respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface.
Widgets can be as simple as a piece of text, a button, or a fully-fledge component like a text editor or file browser (which may contain widgets of their own).
Widgets can be as simple as a piece of text, a button, or a fully-fledged component like a text editor or file browser (which may contain widgets of their own).
### Composing
To add widgets to your app implement a [`compose()`][textual.app.App.compose] method which should return an iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a *generator*.
To add widgets to your app implement a [`compose()`][textual.app.App.compose] method which should return an iterable of `Widget` instances. A list would work, but it is convenient to yield widgets, making the method a *generator*.
The following example imports a builtin Welcome widget and yields it from `App.compose()`.
The following example imports a builtin `Welcome` widget and yields it from `App.compose()`.
```python title="widgets01.py"
--8<-- "docs/examples/app/widgets01.py"
```
When you run this code, Textual will *mount* the Welcome widget which contains Markdown content and a button:
When you run this code, Textual will *mount* the `Welcome` widget which contains Markdown content and a button:
```{.textual path="docs/examples/app/widgets01.py"}
```
Notice the `on_button_pressed` method which handles the [Button.Pressed][textual.widgets.Button] event sent by a button contained in the Welcome widget. The handler calls [App.exit()][textual.app.App.exit] to exit the app.
Notice the `on_button_pressed` method which handles the [Button.Pressed][textual.widgets.Button] event sent by a button contained in the `Welcome` widget. The handler calls [App.exit()][textual.app.App.exit] to exit the app.
### Mounting
@@ -141,7 +141,7 @@ You may have noticed that we subclassed `App[str]` rather than the usual `App`.
--8<-- "docs/examples/app/question01.py"
```
The addition of `[str]` tells Mypy that `run()` is expected to return a string. It may also return `None` if [App.exit()][textual.app.App.exit] is called without a return value, so the return type of `run` will be `str | None`. Replace the `str` in `[str]` with the type of the value you intend to call the exit method with.
The addition of `[str]` tells mypy that `run()` is expected to return a string. It may also return `None` if [App.exit()][textual.app.App.exit] is called without a return value, so the return type of `run` will be `str | None`. Replace the `str` in `[str]` with the type of the value you intend to call the exit method with.
!!! note
@@ -151,7 +151,7 @@ The addition of `[str]` tells Mypy that `run()` is expected to return a string.
Textual apps can reference [CSS](CSS.md) files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy).
The chapter on [Textual CSS](CSS.md) describes how to use CSS in detail. For now lets look at how your app references external CSS files.
The chapter on [Textual CSS](CSS.md) describes how to use CSS in detail. For now let's look at how your app references external CSS files.
The following example enables loading of CSS by adding a `CSS_PATH` class variable:
@@ -184,4 +184,4 @@ Here's the question app with classvar CSS:
## What's next
In the following chapter we will learn more about how to apply styles to you widgets and app.
In the following chapter we will learn more about how to apply styles to your widgets and app.

View File

@@ -23,7 +23,9 @@ You can run Textual apps with the `run` subcommand. If you supply a path to a Py
textual run my_app.py
```
The `run` sub-command assumes you have an App instance called `app` in the global scope of your Python file. If the application is called something different, you can specify it with a colon following the filename:
The `run` sub-command will first look for a `App` instance called `app` in the global scope of your Python file. If there is no `app`, it will create an instance of the first `App` class it finds and run that.
Alternatively, you can add the name of an `App` instance or class after a colon to run a specific app in the Python file. Here's an example:
```bash
textual run my_app.py:alternative_app
@@ -103,7 +105,7 @@ log("[bold red]DANGER![/] We're having too much fun")
### Log method
There's a convenient shortcut to `log` available on the App and Widget objects. This is useful in event handlers. Here's an example:
There's a convenient shortcut to `log` available on the `App` and `Widget` objects. This is useful in event handlers. Here's an example:
```python
from textual.app import App
@@ -120,4 +122,3 @@ if __name__ == "__main__":
LogApp.run()
```

View File

@@ -69,7 +69,7 @@ After Textual calls `Button.on_key` the event _bubbles_ to the button's parent a
--8<-- "docs/images/events/bubble2.excalidraw.svg"
</div>
As before, the event bubbles to it's parent (the App class).
As before, the event bubbles to its parent (the App class).
<div class="excalidraw">
--8<-- "docs/images/events/bubble3.excalidraw.svg"
@@ -186,4 +186,4 @@ Let's look at an example which looks up word definitions from an [api](https://d
```{.textual path="docs/examples/events/dictionary.py" press="t,e,x,t,_,_,_,_,_,_,_,_,_,_,_"}
```
Note the highlighted line in the above code which calls `asyncio.create_task` to run coroutine in the background. Without this you would find typing in to the text box to be unresponsive.
Note the highlighted line in the above code which calls `asyncio.create_task` to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive.

View File

@@ -10,7 +10,7 @@ This chapter will discuss how to make your app respond to input in the form of k
## Keyboard input
The most fundamental way to receive input in via [Key](./events/key) events. Let's write an app to show key events as you type.
The most fundamental way to receive input is via [Key](./events/key) events. Let's write an app to show key events as you type.
=== "key01.py"
@@ -23,13 +23,13 @@ The most fundamental way to receive input in via [Key](./events/key) events. Let
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"}
```
Note the key event handler on the app which logs all key events. if you press any key it will show up on the screen.
Note the key event handler on the app which logs all key events. If you press any key it will show up on the screen.
### Attributes
There are two main attributes on a key event. The `key` attribute is the _name_ of the key which may be a single character, or a longer identifier. Textual ensures that the `key` attribute could always be used in a method name.
Key events also contain a `char` attribute which contains single character if it is printable, or ``None`` if it is not printable (like a function key which has no corresponding character).
Key events also contain a `char` attribute which contains a single character if it is printable, or ``None`` if it is not printable (like a function key which has no corresponding character).
To illustrate the difference between `key` and `char`, try `key01.py` with the space key. You should see something like the following:
@@ -78,7 +78,7 @@ The following example shows how focus works in practice.
```{.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 thw 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 thw widget has focus. Key events will be sent to the focused widget only.
!!! tip
@@ -156,7 +156,7 @@ Coordinates may be relative to the screen, so `(0, 0)` would be the top left of
### Mouse movements
When you move the mouse cursor over a widget it will receive [MouseMove](../events/mouse_move.md) events which contain the coordinate of the mouse and information about what modified keys (++ctrl++, ++shift++ etc).
When you move the mouse cursor over a widget it will receive [MouseMove](../events/mouse_move.md) events which contain the coordinate of the mouse and information about what modifier keys (++ctrl++, ++shift++ etc) are held down.
The following example shows mouse movements being used to _attach_ a widget to the mouse cursor.
@@ -200,7 +200,7 @@ If you want your app to respond to a mouse click you should prefer the Click eve
### Scroll events
Most mice have a scroll wheel which you can use to scroll window underneath the cursor. Scrollable containers in Textual will handle these automatically, but you can handle [MouseDown](../events/mouse_scroll_down.md) and [MouseUp](../events/mouse_scroll_up) if you want build your own scrolling functionality.
Most mice have a scroll wheel which you can use to scroll the window underneath the cursor. Scrollable containers in Textual will handle these automatically, but you can handle [MouseScrollDown](../events/mouse_scroll_down.md) and [MouseScrollUp](../events/mouse_scroll_up) if you want build your own scrolling functionality.
!!! information

View File

@@ -468,7 +468,7 @@ If we wished for the sidebar to appear below the header, it'd simply be a case o
## Layers
Textual has a concept of _layers_ which gives you finely grained control over the order widgets are place.
Textual has a concept of _layers_ which gives you finely grained control over the order widgets are placed.
When drawing widgets, Textual will first draw on _lower_ layers, working its way up to higher layers.
As such, widgets on higher layers will be drawn on top of those on lower layers.
@@ -548,4 +548,4 @@ The example below shows how an advanced layout can be built by combining the var
--8<-- "docs/examples/guide/layout/combining_layouts.css"
```
Textual layouts make it easy design build real-life applications with relatively little code.
Textual layouts make it easy to design and build real-life applications with relatively little code.

View File

@@ -35,7 +35,7 @@ The `reactive` constructor accepts a default value as the first positional argum
Textual uses Python's _descriptor protocol_ to create reactive attributes, which is the same protocol used by the builtin `property` decorator.
You can get and set these attributes in the same way as if you had assigned them in a `__init__` method. For instance `self.name = "Jessica"`, `self.count += 1`, or `print(self.is_cool)`.
You can get and set these attributes in the same way as if you had assigned them in an `__init__` method. For instance `self.name = "Jessica"`, `self.count += 1`, or `print(self.is_cool)`.
### Dynamic defaults
@@ -69,7 +69,7 @@ The first superpower we will look at is "smart refresh". When you modify a react
!!! information
If you modify multiple reactive attribute, Textual will only do a single refresh to minimize updates.
If you modify multiple reactive attributes, Textual will only do a single refresh to minimize updates.
Let's look at an example which illustrates this. In the following app, the value of an input is used to update a "Hello, World!" type greeting.
@@ -167,7 +167,7 @@ If you click the buttons in the above example it will show the current count. Wh
Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch methods begin with `watch_` followed by the name of the attribute. If the watch method accepts a positional argument, it will be called with the new assigned value. If the watch method accepts *two* positional arguments, it will be called with both the *old* value and the *new* value.
The follow app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example `"darkorchid"` or `"#52de44".
The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example `"darkorchid"` or `"#52de44"`.
=== "watch01.py"
@@ -198,7 +198,7 @@ Compute methods are the final superpower offered by the `reactive` descriptor. T
You could be forgiven in thinking this sounds a lot like Python's property decorator. The difference is that Textual will cache the value of compute methods, and update them when any other reactive attribute changes.
The following example uses a computed attribute. It displays three inputs for the each color component (red, green, and blue). If you enter numbers in to these inputs, the background color of another widget changes.
The following example uses a computed attribute. It displays three inputs for each color component (red, green, and blue). If you enter numbers in to these inputs, the background color of another widget changes.
=== "computed01.py"

View File

@@ -39,15 +39,15 @@ Let's look at a simple example of writing a screen class to simulate Window's [b
```{.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.
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.
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.
The app class has a new `SCREENS` class variable. Textual uses this class variable to associate 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 associate a screen with a name by defining a `SCREENS` class variable in your app, which should be a `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.
@@ -68,7 +68,7 @@ You can also _install_ new named screens dynamically with the [install_screen][t
```{.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.
Although both do the same thing, we recommend `SCREENS` for screens that exist for the lifetime of your app.
### Uninstalling screens

View File

@@ -1,6 +1,6 @@
# Styles
In this chapter will explore how you can apply styles to your application to create beautiful user interfaces.
In this chapter we will explore how you can apply styles to your application to create beautiful user interfaces.
## Styles object
@@ -48,9 +48,9 @@ Note how the combined height of the widget is three rows in the terminal. This i
## Colors
There are a number of style attribute which accept colors. The most commonly used are [color](../styles/color.md) which sets the default color of text on a widget, and [background](..styles/background/md) which sets the background color (beneath the text).
There are a number of style attributes which accept colors. The most commonly used are [color](../styles/color.md) which sets the default color of text on a widget, and [background](..styles/background/md) which sets the background color (beneath the text).
You can set a color value to one of a number of pre-defined color constants, such as "crimson", "lime", and "palegreen". You can find a full list in the [Color reference](../reference/color.md#textual.color--named-colors).
You can set a color value to one of a number of pre-defined color constants, such as `"crimson"`, `"lime"`, and `"palegreen"`. You can find a full list in the [Color reference](../reference/color.md#textual.color--named-colors).
Here's how you would set the screen background to lime:
@@ -90,13 +90,13 @@ There are a few ways you can set alpha on a color in Textual.
- You can also set alpha with the `rgba` format, which is identical to `rgb` with the additional of a fourth value that should be between 0 and 1, where 0 is invisible and 1 is opaque. For example `"rgba(192,78,96,0.5)"`.
- You can add the `a` parameter on a [Color][textual.color.Color] object. For example `Color(192, 78, 96, a=0.5)` creates a translucent dark orchid.
The following examples shows what happens when you set alpha on background colors:
The following example shows what happens when you set alpha on background colors:
```python title="colors01.py" hl_lines="12-15"
--8<-- "docs/examples/guide/styles/colors02.py"
```
Notice that an alpha of 0.1 the background almost matches the screen, but at 1.0 it is a solid color.
Notice that at an alpha of 0.1 the background almost matches the screen, but at 1.0 it is a solid color.
```{.textual path="docs/examples/guide/styles/colors02.py"}
```
@@ -138,7 +138,7 @@ Note how the text wraps in the widget, and is cropped because it doesn't fit in
#### Auto dimensions
In practice, we generally want the size of a widget to adapt to it's content, which we can do by setting a dimension to `"auto"`.
In practice, we generally want the size of a widget to adapt to its content, which we can do by setting a dimension to `"auto"`.
Let's set the height to auto and see what happens.
@@ -195,7 +195,7 @@ Let's look at an example. We will create two widgets, one with a height of `"2fr
--8<-- "docs/examples/guide/styles/dimensions04.py"
```
The total `fr` units for height is 3. The first widget will have a screen height of two thirds because its height style is set to `2fr`. The second widget's height styles is `1fr` so its screen height will be one third. Here's what that looks like.
The total `fr` units for height is 3. The first widget will have a screen height of two thirds because its height style is set to `2fr`. The second widget's height style is `1fr` so its screen height will be one third. Here's what that looks like.
```{.textual path="docs/examples/guide/styles/dimensions04.py"}
```

View File

@@ -15,7 +15,7 @@ A widget is a component of your UI responsible for managing a rectangular region
There is a growing collection of [builtin widgets](../widgets/index.md) in Textual, but you can build entirely custom widgets that work in the same way.
The first step in building a widget is to import and extend a widget class. This can either be [Widget][textual.widget.Widget] which is the base class of all widgets, or one of it's subclasses.
The first step in building a widget is to import and extend a widget class. This can either be [Widget][textual.widget.Widget] which is the base class of all widgets, or one of its subclasses.
Let's create a simple custom widget to display a greeting.
@@ -76,11 +76,11 @@ Let's use Static to create a widget which cycles through "hello" in various lang
Note that there is no `render()` method on this widget. The Static class is handling the render for us. Instead we call `update()` when we want to update the content within the widget.
The `next_word` method updates the greeting. We call this method from the mount handler to get the first word, and from an click handler to cycle through the greetings when we click the widget.
The `next_word` method updates the greeting. We call this method from the mount handler to get the first word, and from a click handler to cycle through the greetings when we click the widget.
### Default CSS
When building an app it is best to keep your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you intent to distribute a widget (via PyPI for instance) it can be convenient to bundle the code and CSS together. You can do this by adding a `DEFAULT_CSS` class variable inside your widget class.
When building an app it is best to keep your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you intend to distribute a widget (via PyPI for instance) it can be convenient to bundle the code and CSS together. You can do this by adding a `DEFAULT_CSS` class variable inside your widget class.
Textual's builtin widgets bundle CSS in this way, which is why you can see nicely styled widgets without having to copy any CSS code.
@@ -191,7 +191,7 @@ Let's modify the default width for the fizzbuzz example. By default, the table w
```{.textual path="docs/examples/guide/widgets/fizzbuzz02.py"}
```
Note that we've added `expand=True` to tell the Table to expand beyond the optimal width, so that it fills the 50 characters returned by `get_content_width`.
Note that we've added `expand=True` to tell the `Table` to expand beyond the optimal width, so that it fills the 50 characters returned by `get_content_width`.
## Compound widgets

View File

@@ -17,7 +17,7 @@ display: [none|block];
## Example
Note that the second widget is hidden by adding the "hidden" class which sets the display style to None.
Note that the second widget is hidden by adding the `"remove"` class which sets the display style to None.
=== "display.py"

View File

@@ -43,7 +43,7 @@ For example, `heavy white` would display a heavy white line around a widget.
## Example
This examples shows a widget with an outline. Note how the outline occludes the text area.
This example shows a widget with an outline. Note how the outline occludes the text area.
=== "outline.py"

View File

@@ -12,7 +12,7 @@ scrollbar-size: <INTEGER> <INTEGER>;
## Example
In this example we modify the size of the widgets scrollbar to be _much_ larger than usual.
In this example we modify the size of the widget's scrollbar to be _much_ larger than usual.
=== "scrollbar_size.py"

View File

@@ -64,7 +64,7 @@ python stopwatch.py
Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects.
We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like [Mypy](https://mypy.readthedocs.io/en/stable/) to catch bugs before your code runs.
We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like [mypy](https://mypy.readthedocs.io/en/stable/) to catch bugs before your code runs.
The following function contains type hints:
@@ -102,7 +102,7 @@ Hit ++ctrl+c++ to exit the app and return to the command prompt.
### A closer look at the App class
Let's examine stopwatch01.py in more detail.
Let's examine `stopwatch01.py` in more detail.
```python title="stopwatch01.py" hl_lines="1 2"
--8<-- "docs/examples/tutorial/stopwatch01.py"
@@ -157,7 +157,7 @@ Let's add those to the app. Just a skeleton for now, we will add the rest of the
--8<-- "docs/examples/tutorial/stopwatch02.py"
```
We've imported two new widgets in this code: `Button`, which creates a clickable button, and `Static` which is a base class for a simple control. We've also imported `Container` from `textual.containers` which (as the name suggests) is a Widget which contains other widgets.
We've imported two new widgets in this code: `Button`, which creates a clickable button, and `Static` which is a base class for a simple control. We've also imported `Container` from `textual.containers` which (as the name suggests) is a `Widget` which contains other widgets.
We've defined an empty `TimeDisplay` widget by extending `Static`. We will flesh this out later.
@@ -179,7 +179,7 @@ The new line in `Stopwatch.compose()` yields a single `Container` object which w
### The unstyled app
Let's see what happens when we run "stopwatch02.py".
Let's see what happens when we run `stopwatch02.py`.
```{.textual path="docs/examples/tutorial/stopwatch02.py" title="stopwatch02.py"}
```
@@ -222,7 +222,7 @@ If we run the app now, it will look *very* different.
```{.textual path="docs/examples/tutorial/stopwatch03.py" title="stopwatch03.py"}
```
This app looks much more like our sketch. let's look at how the Textual uses `stopwatch03.css` to apply styles.
This app looks much more like our sketch. Let's look at how the Textual uses `stopwatch03.css` to apply styles.
### CSS basics
@@ -250,7 +250,7 @@ Here's how this CSS code changes how the `Stopwatch` widget is displayed.
- `background: $boost` sets the background color to `$boost`. The `$` prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as `"blue"` or `rgb(20,46,210)`.
- `height: 5` sets the height of our widget to 5 lines of text.
- `padding: 1` sets a padding of 1 cell around the child widgets.
- `margin: 1` sets a margin of 1 cell around the Stopwatch widget to create a little space between widgets in the list.
- `margin: 1` sets a margin of 1 cell around the `Stopwatch` widget to create a little space between widgets in the list.
Here's the rest of `stopwatch03.css` which contains further declaration blocks:
@@ -284,7 +284,7 @@ The `TimeDisplay` block aligns text to the center (`content-align`), fades it sl
The `Button` block sets the width (`width`) of buttons to 16 cells (character widths).
The last 3 blocks have a slightly different format. When the declaration begins with a `#` then the styles will be applied to widgets with a matching "id" attribute. We've set an ID on the Button widgets we yielded in compose. For instance the first button has `id="start"` which matches `#start` in the CSS.
The last 3 blocks have a slightly different format. When the declaration begins with a `#` then the styles will be applied to widgets with a matching "id" attribute. We've set an ID on the `Button` widgets we yielded in `compose`. For instance the first button has `id="start"` which matches `#start` in the CSS.
The buttons have a `dock` style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge.
@@ -292,7 +292,7 @@ You may have noticed that the stop button (`#stop` in the CSS) has `display: non
### Dynamic CSS
We want our Stopwatch widget to have two states: a default state with a Start and Reset button; and a _started_ state with a Stop button. When a stopwatch is started it should also have a green background and bold text.
We want our `Stopwatch` widget to have two states: a default state with a Start and Reset button; and a _started_ state with a Stop button. When a stopwatch is started it should also have a green background and bold text.
<div class="excalidraw">
--8<-- "docs/images/css_stopwatch.excalidraw.svg"
@@ -317,7 +317,7 @@ Some of the new styles have more than one selector separated by a space. The spa
}
```
The `.started` selector matches any widget with a `"started"` CSS class. While `#start` matches a child widget with an ID of "start". So it matches the Start button only for Stopwatches in a started state.
The `.started` selector matches any widget with a `"started"` CSS class. While `#start` matches a child widget with an ID of `"start"`. So it matches the Start button only for Stopwatches in a started state.
The rule is `"display: none"` which tells Textual to hide the button.
@@ -335,7 +335,7 @@ The following code will start or stop the stopwatches in response to clicking a
The `on_button_pressed` method is an *event handler*. Event handlers are methods called by Textual in response to an *event* such as a key press, mouse click, etc. Event handlers begin with `on_` followed by the name of the event they will handler. Hence `on_button_pressed` will handle the button pressed event.
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,_,_,_"}
```
@@ -350,7 +350,7 @@ You can declare a reactive attribute with [reactive][textual.reactive.reactive].
--8<-- "docs/examples/tutorial/stopwatch05.py"
```
We have added two reactive attributes: `start_time` will contain the time in seconds when the stopwatch was started, and `time` will contain the time to be displayed on the Stopwatch.
We have added two reactive attributes: `start_time` will contain the time in seconds when the stopwatch was started, and `time` will contain the time to be displayed on the `Stopwatch`.
Both attributes will be available on `self` as if you had assigned them in `__init__`. If you write to either of these attributes the widget will update automatically.
@@ -374,7 +374,7 @@ The end result is that the `Stopwatch` widgets show the time elapsed since the w
```{.textual path="docs/examples/tutorial/stopwatch05.py" title="stopwatch05.py"}
```
We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate Stopwatches independently.
We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate stopwatches independently.
### Wiring buttons
@@ -414,9 +414,9 @@ This code supplies missing features and makes our app useful. We've made the fol
- The first line retrieves `id` attribute of the button that was pressed. We can use this to decide what to do in response.
- The second line calls `query_one` to get a reference to the `TimeDisplay` widget.
- We call the method on `TimeDisplay` that matches the pressed button.
- We add the "started" class when the Stopwatch is started (`self.add_class("started)`), and remove it (`self.remove_class("started")`) when it is stopped. This will update the Stopwatch visuals via CSS.
- We add the `"started"` class when the Stopwatch is started (`self.add_class("started")`), and remove it (`self.remove_class("started")`) when it is stopped. This will update the Stopwatch visuals via CSS.
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"}
```
@@ -435,12 +435,12 @@ 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.
- 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.
The `action_add_stopwatch` method creates and mounts a new stopwatch. Note the call to [query_one()][textual.dom.DOMNode.query_one] with a CSS selector of `"#timers"` which gets the timer's container via its ID. Once mounted, the new Stopwatch will appear in the terminal. That last line in `action_add_stopwatch` calls [scroll_visible()][textual.widget.Widget.scroll_visible] which will scroll the container to make the new Stopwatch visible (if required).
The `action_add_stopwatch` method creates and mounts a new stopwatch. Note the call to [query_one()][textual.dom.DOMNode.query_one] with a CSS selector of `"#timers"` which gets the timer's container via its ID. Once mounted, the new Stopwatch will appear in the terminal. That last line in `action_add_stopwatch` calls [scroll_visible()][textual.widget.Widget.scroll_visible] which will scroll the container to make the new `Stopwatch` visible (if required).
The `action_remove_stopwatch` function calls [query()][textual.dom.DOMNode.query] with a CSS selector of `"Stopwatch"` which gets all the `Stopwatch` widgets. If there are stopwatches then the action calls [last()][textual.css.query.DOMQuery.last] to get the last stopwatch, and [remove()][textual.css.query.DOMQuery.remove] to remove it.
@@ -451,6 +451,6 @@ If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and
## What next?
Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples.
Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak `stopwatch.py` or look through the examples.
Read the guide for the full details on how to build sophisticated TUI applications with Textual.

View File

@@ -89,11 +89,11 @@ nav:
- "styles/visibility.md"
- "styles/width.md"
- Widgets:
- "widgets/index.md"
- "widgets/button.md"
- "widgets/data_table.md"
- "widgets/footer.md"
- "widgets/header.md"
- "widgets/index.md"
- "widgets/input.md"
- "widgets/static.md"
- "widgets/tree_control.md"

View File

@@ -14,6 +14,7 @@ class JustABox(App):
key="o,f,w",
action="widget_fade_out",
description="opacity fade out",
key_display="o or f or w",
),
]

View File

@@ -325,10 +325,10 @@ class App(Generic[ReturnType], DOMNode):
@property
def bindings(self) -> Bindings:
"""Get current bindings. If no widget is focused, then the app-level bindings
are returned. If a widget is focused, then any bindings present between that widget
and the App in the DOM are merged and returned."""
are returned. If a widget is focused, then any bindings present in the active
screen and app are merged and returned."""
if self.focused is None:
return self._bindings
return Bindings.merge([self.screen._bindings, self._bindings])
else:
return Bindings.merge(
node._bindings for node in reversed(self.focused.ancestors)
@@ -483,7 +483,7 @@ class App(Generic[ReturnType], DOMNode):
self.dark = not self.dark
def action_screenshot(self, filename: str | None, path: str = "~/") -> None:
"""Save an SVG "screenshot". This action will save a SVG file containing the current contents of the screen.
"""Save an SVG "screenshot". This action will save an SVG file containing the current contents of the screen.
Args:
filename (str | None, optional): Filename of screenshot, or None to auto-generate. Defaults to None.
@@ -492,7 +492,7 @@ class App(Generic[ReturnType], DOMNode):
self.save_screenshot(filename, path)
def export_screenshot(self, *, title: str | None = None) -> str:
"""Export a SVG screenshot of the current screen.
"""Export an SVG screenshot of the current screen.
Args:
title (str | None, optional): The title of the exported screenshot or None
@@ -519,7 +519,7 @@ class App(Generic[ReturnType], DOMNode):
path: str = "./",
time_format: str = "%Y-%m-%d %X %f",
) -> str:
"""Save a SVG screenshot of the current screen.
"""Save an SVG screenshot of the current screen.
Args:
filename (str | None, optional): Filename of SVG screenshot, or None to auto-generate

View File

@@ -47,26 +47,14 @@ class Bindings:
for binding in bindings:
if isinstance(binding, Binding):
binding_keys = binding.key.split(",")
# If there's a key display, split it and associate it with the keys
key_displays = (
binding.key_display.split(",") if binding.key_display else []
)
if len(binding_keys) == len(key_displays):
keys_and_displays = zip(binding_keys, key_displays)
else:
keys_and_displays = [
(key, binding.key_display) for key in binding_keys
]
if len(binding_keys) > 1:
for key, display in keys_and_displays:
for key in binding_keys:
new_binding = Binding(
key=key,
action=binding.action,
description=binding.description,
show=binding.show,
key_display=display,
key_display=binding.key_display,
allow_forward=binding.allow_forward,
)
yield new_binding

View File

@@ -148,7 +148,7 @@ class Hide(Event, bubble=False):
class MouseCapture(Event, bubble=False):
"""Sent when the mouse has been captured.
When a mouse has been captures, all further mouse events will be sent to the capturing widget.
When a mouse has been captured, all further mouse events will be sent to the capturing widget.
Args:

View File

@@ -2,10 +2,9 @@ from __future__ import annotations
from collections import defaultdict
from rich.console import RenderableType
from rich.text import Text
import rich.repr
from rich.console import RenderableType
from rich.text import Text
from .. import events
from ..reactive import Reactive, watch
@@ -95,14 +94,12 @@ class Footer(Widget):
action_to_bindings[binding.action].append(binding)
for action, bindings in action_to_bindings.items():
key_displays = [
binding = bindings[0]
key_display = (
binding.key.upper()
if binding.key_display is None
else binding.key_display
for binding in bindings
]
key_display = "·".join(key_displays)
binding = bindings[0]
)
hovered = self.highlight_key == binding.key
key_text = Text.assemble(
(f" {key_display} ", highlight_key_style if hovered else key_style),

View File

@@ -107,6 +107,7 @@ class Input(Widget, can_focus=True):
value: str = "",
placeholder: str = "",
highlighter: Highlighter | None = None,
password: bool = False,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
@@ -115,6 +116,7 @@ class Input(Widget, can_focus=True):
self.value = value
self.placeholder = placeholder
self.highlighter = highlighter
self.password = password
def _position_to_cell(self, position: int) -> int:
"""Convert an index within the value to cell position."""