introduction docs

This commit is contained in:
Will McGugan
2022-08-19 22:06:08 +01:00
parent 5d8fafc74d
commit 1a2997aa73
9 changed files with 126 additions and 184 deletions

View File

@@ -1,7 +1,6 @@
TimerWidget {
Stopwatch {
layout: horizontal;
background: $panel-darken-1;
height: 5;
min-width: 50;
margin: 1;
@@ -9,7 +8,7 @@ TimerWidget {
transition: background 300ms linear;
}
TimerWidget.started {
Stopwatch.started {
text-style: bold;
background: $success;
color: $text-success;
@@ -22,7 +21,7 @@ TimeDisplay {
height: 3;
}
TimerWidget.started TimeDisplay {
Stopwatch.started TimeDisplay {
opacity: 100%;
}
@@ -43,15 +42,15 @@ Button {
dock: right;
}
TimerWidget.started #start {
Stopwatch.started #start {
display: none
}
TimerWidget.started #stop {
Stopwatch.started #stop {
display: block
}
TimerWidget.started #reset {
Stopwatch.started #reset {
visibility: hidden
}

View File

@@ -9,16 +9,16 @@ from textual.widgets import Button, Header, Footer, Static
class TimeDisplay(Static):
"""Displays the time."""
time_delta = Reactive(0.0)
time = Reactive(0.0)
def watch_time_delta(self, time_delta: float) -> None:
def watch_time(self, time: float) -> None:
"""Called when time_delta changes."""
minutes, seconds = divmod(time_delta, 60)
minutes, seconds = divmod(time, 60)
hours, minutes = divmod(minutes, 60)
self.update(f"{hours:02.0f}:{minutes:02.0f}:{seconds:05.2f}")
class TimerWidget(Static):
class Stopwatch(Static):
"""The timer widget (display + buttons)."""
start_time = Reactive(0.0)
@@ -31,7 +31,7 @@ class TimerWidget(Static):
def update_elapsed(self) -> None:
"""Updates elapsed time."""
self.query_one(TimeDisplay).time_delta = (
self.query_one(TimeDisplay).time = (
self.total + monotonic() - self.start_time if self.started else self.total
)
@@ -64,7 +64,7 @@ class TimerWidget(Static):
self.update_elapsed()
class TimerApp(App):
class StopwatchApp(App):
"""Manage the timers."""
def on_load(self) -> None:
@@ -77,11 +77,11 @@ class TimerApp(App):
"""Called to ad widgets to the app."""
yield Header()
yield Footer()
yield Container(TimerWidget(), TimerWidget(), TimerWidget(), id="timers")
yield Container(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")
def action_add_timer(self) -> None:
"""An action to add a timer."""
new_timer = TimerWidget()
new_timer = Stopwatch()
self.query_one("#timers").mount(new_timer)
new_timer.scroll_visible()
@@ -95,6 +95,6 @@ class TimerApp(App):
self.dark = not self.dark
app = TimerApp(css_path="timers.css")
app = StopwatchApp(css_path="stopwatch.css")
if __name__ == "__main__":
app.run()

View File

@@ -2,7 +2,7 @@ from textual.app import App
from textual.widgets import Header, Footer
class TimerApp(App):
class StopwatchApp(App):
def compose(self):
yield Header()
yield Footer()
@@ -14,6 +14,6 @@ class TimerApp(App):
self.dark = not self.dark
app = TimerApp()
app = StopwatchApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1 @@
/* Blank for now */

View File

@@ -0,0 +1,33 @@
from textual.app import App, ComposeResult
from textual.layout import Container
from textual.widgets import Button, Header, Footer, Static
class TimeDisplay(Static):
pass
class Stopwatch(Static):
def compose(self) -> ComposeResult:
yield Button("Start", id="start", variant="success")
yield Button("Stop", id="stop", variant="error")
yield Button("Reset", id="reset")
yield TimeDisplay("00:00:00.00")
class StopwatchApp(App):
def compose(self):
yield Header()
yield Footer()
yield Container(Stopwatch(), Stopwatch(), Stopwatch())
def on_load(self):
self.bind("d", "toggle_dark", description="Dark mode")
def action_toggle_dark(self):
self.dark = not self.dark
app = StopwatchApp(css_path="stopwatch02.css")
if __name__ == "__main__":
app.run()

View File

@@ -21,3 +21,6 @@ If you installed the dev dependencies, you have have access to the `textual` CLI
```python
textual --help
```
### Textual Console

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -14,7 +14,13 @@ This is a simple yet **fully featured** app — you could distribute this ap
Here's what the finished app will look like:
```{.textual path="docs/examples/introduction/timers.py"}
```{.textual path="docs/examples/introduction/stopwatch.py"}
```
If you want to try this out before reading the rest of this introduction (we recommend it), navigate to "docs/examples/introduction" within the repository and run the following:
```bash
python stopwatch.py
```
## The App class
@@ -75,194 +81,77 @@ There are three methods in our stopwatch app currently.
--8<-- "docs/examples/introduction/stopwatch01.py"
```
The last lines in "stopwatch01.py" may be familiar to you. We create an instance of our app class, and run it within a `__name__ == "__main__"` conditional block. This is so that we could import `app` if we want to. Or we could run it with `python stopwatch01.py`.
The last lines in "stopwatch01.py" may be familiar to you. We create an instance of our app class, and call `run()` within a `__name__ == "__main__"` conditional block. This is so that we could import `app` if we want to. Or we could run it with `python stopwatch01.py`.
## Timers
## Creating a custom widget
=== "Timers Python"
The header and footer were builtin widgets. For our stopwatch application we will need to build a custom widget for stopwatches.
```python title="timers.py"
--8<-- "docs/examples/introduction/timers.py"
```
Let's sketch out what we are trying to achieve here:
=== "Timers CSS"
```python title="timers.css"
--8<-- "docs/examples/introduction/timers.css"
```
<div class="excalidraw">
--8<-- "docs/images/stopwatch.excalidraw.svg"
</div>
## Pre-requisites
An individual stopwatch consists of several parts, which themselves can be widgets.
- Python 3.7 or later. If you have a choice, pick the most recent version.
- Installed `textual` from Pypi.
- Basic Python skills.
Out stopwatch widgets is going to need the following widgets:
- A "start" button
- A "stop" button
- A "reset" button
- A time display
```{.textual path="docs/examples/introduction/timers.py"}
Textual has a builtin `Button` widgets which takes care of the first three components. All we need to build is the time display which will show the elapsed time in HOURS:MINUTES:SECONDS format, and the stopwatch itself.
```
Let's add those to our app:
## A Simple App
Let's looks at the simplest possible Textual app.
If you would like to follow along and run the examples, navigate to the `docs/examples/introduction` directory from the command prompt. We will be looking at `intro01.py`, which you can see here:
```python title="intro01.py"
--8<-- "docs/examples/introduction/intro01.py"
```python title="stopwatch02.py" hl_lines="3 6-7 10-15 22 31"
--8<-- "docs/examples/introduction/stopwatch02.py"
```
Enter the following command to run the application:
### New widgets
```bash
python intro01.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.layout`. As the name suggests, `Container` is a Widget which contains other widgets. We will use this container to form a scrolling list of stopwatches.
We're extending Static as a foundation for our `TimeDisplay` widget. There are no methods on this class yet.
The Stopwatch also extends Static to define a new widget. This class has a `compose()` method which yields its _child_ widgets, consisting of of three `Button` objects and a single `TimeDisplay`. These are all we need to build a stopwatch like the sketch.
The Button constructor takes a label to be displayed to the user ("Start", "Stop", or "Reset") so they know what will happen when they click on it. There are two additional parameters to the Button constructor we are using:
- **`id`** is an identifier so we can tell the buttons apart in code. We can also use this to style the buttons. More on that later.
- **`variant`** is a string which selects a default style. The "success" variant makes the button green, and the "error" variant makes it red.
### Composing the widgets
To see our widgets with we need to yield them from the app's `compose()` method:
This new line in `Stopwatch.compose()` adds a single `Container` object which will create a scrolling list. The constructor for `Container` takes its _child_ widgets as positional arguments, to which we pass three instances of the `Stopwatch` we just built.
### Setting the CSS path
The `StopwatchApp` constructor has a new argument: `css_path` is set to the file `stopwatch02.css` which is blank:
```python title="stopwatch02.css"
--8<-- "docs/examples/introduction/stopwatch02.css"
```
The command prompt should disappear and you will see a blank screen:
### The unstyled app
```{.textual path="docs/examples/introduction/intro01.py"}
Let's see what happens when we run "stopwatch02.py":
```{.textual path="docs/examples/introduction/stopwatch02.py" title="stopwatch02.py"}
```
Hit ++Ctrl+c++ to exit and return to the command prompt.
The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container, but it doesn't look much like the sketch. This is because we have yet to add any _styles_ to the CSS file.
### Application mode
The first step in all Textual applications is to import the `App` class from `textual.app` and extend it:
```python hl_lines="1 2 3 4 5" title="intro01.py"
--8<-- "docs/examples/introduction/intro01.py"
```
This App class is responsible for loading data, setting up the screen, managing events etc. In a real app most of the core logic of your application will be contained within methods on this class.
The last two lines create an instance of the application and call the `run()` method:
```python hl_lines="8 9" title="intro01.py"
--8<-- "docs/examples/introduction/intro01.py"
```
The `run` method will put your terminal in to "application mode" which disables the prompt and allows Textual to take over input and output. When you press ++ctrl+c++ the application will exit application mode and re-enable the command prompt.
## Handling Events
Most real-world applications will need to interact with the user in some way. To do this we can make use of _event handler_ methods, which are called in response to things the user does such as pressing keys, moving the mouse, resizing the terminal, etc.
Each event type is represented by an instance of one of a number of Event objects. These event objects may contain additional information regarding the event. For instance, the `Key` event contains the key the user pressed and a `Mouse` event will contain the coordinates of the mouse cursor.
Textual uses CSS files to define what widgets look like. With CSS we can apply styles for color, borders, alignment, positioning, animation, and more.
!!! note
Although `intro01.py` did not explicitly define any event handlers, Textual still had to respond to events to catch ++ctrl+c++, otherwise you wouldn't be able to exit the app.
Don't worry if you have never worked with CSS before. The dialect of CSS we use is greatly simplified over web based CSS and easy to learn!
The next example demonstrates handling events. Try running `intro02.py` in the `docs/examples/introduction` directory:
## Writing Textual CSS
```python title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
```
When you run this app you should see a blue screen in your terminal, like the following:
```{.textual path="docs/examples/introduction/intro02.py"}
```
If you hit any of the number keys ++0++-++9++, the background will change color and you should hear a beep. As before, pressing ++ctrl+c++ will exit the app and return you to your prompt.
!!! note
The "beep" is your terminal's *bell*. Some terminals may be configured to play different noises or a visual indication of a bell rather than a noise.
There are two event handlers in this app. Event handlers start with the text `on_` followed by the name of the event in lower case. Hence `on_mount` is called for the `Mount` event, and `on_key` is called for the `Key` event.
!!! note
Event class names are transformed to _camel case_ when used in event handlers. So the `MouseMove` event will be handled by a method called `on_mouse_move`.
The first event handler to run is `on_mount`. The `Mount` event is sent to your application immediately after entering application mode.
```python hl_lines="19 20" title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
```
The above `on_mount` method sets the `background` attribute of `self.styles` to `"darkblue"` which makes the background blue when the application starts. There are a lot of other style properties which define how your app looks. We will explore those later.
!!! note
You may have noticed there is no function call to repaint the screen in this example. Textual is smart enough to know when the screen needs to be updated, and will do it automatically.
The second event handler will receive `Key` events whenever you press a key on the keyboard:
```python hl_lines="22 23 24 25" title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
```
This method has an `event` positional argument which will receive the event object; in this case the `Key` event. The body of the method sets the background to a corresponding color in the `COLORS` list when you press one of the digit keys. It also calls `bell()` to plays your terminal's bell sound.
!!! note
Every event has a corresponding `Event` object. Textual will call your event handler with an event object only if you have it in the argument list. It does this by inspecting the handler method prior to calling it. So if you don't need the event object, you may leave it out.
## Widgets
Most Textual applications will make use of one or more `Widget` classes. A Widget is a self contained component responsible for defining how a given part of the screen should look. Widgets respond to events in much the same way as the App does.
Let's look at an app with a simple Widget to show the current time and date. Here is the code for `"clock01.py"` which is in the same directory as the previous examples:
```python title="clock01.py"
--8<-- "docs/examples/introduction/clock01.py"
```
Here's what you will see if you run this code:
```{.textual path="docs/examples/introduction/clock01.py"}
```
This script imports `App` as before and also the `Widget` class from `textual.widget`. To create a Clock widget we extend from the Widget base class.
```python title="clock01.py" hl_lines="7 8 9 10 11 12 13"
--8<-- "docs/examples/introduction/clock01.py"
```
Widgets support many of the same events as the Application itself, and can be thought of as mini-applications in their own right. The Clock widget responds to a Mount event which is the first event received when a widget is _mounted_ (added to the App). The mount handler (`Clock.on_mount`) sets `styles.content_align` to `("center", "middle")` which tells Textual to center align its contents horizontally and vertically. If you size the terminal you should see that the text remains centered.
The second line in `on_mount` calls `self.set_interval` which tells Textual to invoke the `self.refresh` method once per second, so our clock remains up-to-date.
When Textual refreshes a widget it calls it's `render` method:
```python title="clock01.py" hl_lines="12 13"
--8<-- "docs/examples/introduction/clock01.py"
```
The Clock's `render` method uses the datetime module to format the current date and time. It returns a string, but can also return a [Rich](https://github.com/Textualize/rich) _renderable_. Don't worry if you aren't familiar with Rich, we will cover that later.
Before a Widget can be displayed, it must first be mounted on the app. This is typically done within the application's Mount handler:
```python title="clock01.py" hl_lines="17 18"
--8<-- "docs/examples/introduction/clock01.py"
```
In the case of the clock application, we call `mount` with an instance of the `Clock` widget.
That's all there is to this Clock example. It will display the current time until you hit ++ctrl+c++
## Compose
Mounting "child" widgets from from an `on_mount` event is such a common pattern that Textual offers a convenience method to do that.
If you implement a `compose()` method on your App or Widget, Textual will invoke it to get your "sub-widgets". This method should return an _iterable_ such as a list, but you may find it easier to use the `yield` statement to turn it in to a Python generator:
```python title="clock02.py" hl_lines="17 18"
--8<-- "docs/examples/introduction/clock02.py"
```
Here's the clock example again using `compose()` rather than `on_mount`. Any Widgets yielded from this method will be mounted on to the App or Widget. In this case we mount our Clock widget as before.
More sophisticated apps will likely yield multiple widgets from `compose()`, and many widgets will also yield child widgets of their own.
## Next Steps
We've seen how Textual apps can respond to events, and how to mount widgets which are like mini-applications in their own right. These are key concepts in Textual which you can use to build more sophisticated apps.
The Guide covers this in much more detail and describes how arrange widgets on the screen and connect them with the core logic of your application.

View File

@@ -32,6 +32,7 @@ class Button(Widget, can_focus=True):
CSS = """
Button {
width: auto;
min-width: 10;
height: 3;
background: $panel;
color: $text-panel;