mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user