more introduction

This commit is contained in:
Will McGugan
2022-08-21 09:47:42 +01:00
parent 4e4d0b1bb9
commit 25a4812f7a
7 changed files with 77 additions and 36 deletions

View File

@@ -40,9 +40,8 @@ class Stopwatch(Static):
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Called when a button is pressed."""
button_id = event.button.id
self.started = button_id == "start"
if button_id == "reset":
self.started = event.button.id == "start"
if event.button.id == "reset":
self.total = 0.0
self.update_elapsed()

View File

@@ -1,4 +1,4 @@
from textual.app import App, ComposeResult
from textual.app import App
from textual.layout import Container
from textual.widgets import Button, Header, Footer, Static
@@ -8,7 +8,7 @@ class TimeDisplay(Static):
class Stopwatch(Static):
def compose(self) -> ComposeResult:
def compose(self):
yield Button("Start", id="start", variant="success")
yield Button("Stop", id="stop", variant="error")
yield Button("Reset", id="reset")

View File

@@ -8,7 +8,7 @@ class TimeDisplay(Static):
class Stopwatch(Static):
def compose(self) -> ComposeResult:
def compose(self):
yield Button("Start", id="start", variant="success")
yield Button("Stop", id="stop", variant="error")
yield Button("Reset", id="reset")

View File

@@ -30,25 +30,24 @@ Button {
dock: right;
}
Stopwatch.started {
.started {
text-style: bold;
background: $success;
color: $text-success;
}
Stopwatch.started TimeDisplay {
.started TimeDisplay {
opacity: 100%;
}
Stopwatch.started #start {
.started #start {
display: none
}
Stopwatch.started #stop {
.started #stop {
display: block
}
Stopwatch.started #reset {
.started #reset {
visibility: hidden
}

View File

@@ -1,6 +1,5 @@
from textual.app import App, ComposeResult
from textual.app import App
from textual.layout import Container
from textual.reactive import Reactive
from textual.widgets import Button, Header, Footer, Static
@@ -9,20 +8,13 @@ class TimeDisplay(Static):
class Stopwatch(Static):
started = Reactive(False)
def watch_started(self, started: bool) -> None:
if started:
def on_button_pressed(self, event):
if event.button.id == "start":
self.add_class("started")
else:
elif event.button.id == "stop":
self.remove_class("started")
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Called when a button is pressed."""
button_id = event.button.id
self.started = button_id == "start"
def compose(self) -> ComposeResult:
def compose(self):
yield Button("Start", id="start", variant="success")
yield Button("Stop", id="stop", variant="error")
yield Button("Reset", id="reset")

View File

@@ -4,6 +4,10 @@ Welcome to the Textual Introduction!
By the end of this page you should have a good idea of the steps involved in creating an application with Textual.
!!! quote
You may find this page goes in to more detail than you might expect from an introduction. I like to have complete working examples in documentation and I don't want to leave anything _as an exercise for the reader_. — **Will McGugan** (creator of Rich and Textual)
## Stopwatch Application
@@ -17,8 +21,12 @@ Here's what the finished app will look like:
```{.textual path="docs/examples/introduction/stopwatch.py"}
```
### Try the code
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:
**TODO**: instructions how to checkout repo
```bash
python stopwatch.py
```
@@ -111,7 +119,7 @@ Let's add those to our app:
--8<-- "docs/examples/introduction/stopwatch02.py"
```
### New widgets
### Extending widget classes
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 create a scrolling list of stopwatches.
@@ -246,10 +254,45 @@ 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. An _unstarted_ state with a Start and Reset button, and a _started_ state with a Stop button.
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.
There are other visual differences between the two states. When a stopwatch is running it should have a green background and bold text.
We can accomplish this with by defining a _CSS class_. Not to be confused with a Python class, a CSS class is like a tag you can add to a widget to modify its styles.
Here's the new CSS:
```{.textual path="docs/examples/introduction/stopwatch04.py" title="stopwatch04.py" press="tab,enter"}
```sass title="stopwatch04.css" hl_lines="33-53"
--8<-- "docs/examples/introduction/stopwatch04.css"
```
These new rules are prefixed with `.started`. The `.` indicates that `.started` refers to a CSS class called "started". The new styles will be applied only to widgets that have these styles.
Some of the new styles have more than one selector separated by a space. The space indicates that the next selector should match a style. Let's look at one of these styles:
```sass
.started #start {
display: none
}
```
The purpose of this CSS is to hide the start button when the stopwatch is started. The `.started` selector matches any widget with a "started" CSS class. While "#start" matches a child widget with an id of "start". The rule "display: none" tells Textual to hide that widget.
### Manipulating classes
The easiest way to manipulate visuals with Textual is to modify CSS classes. This way your (Python) code can remain free of display related code which tends to be hard to maintain.
You can add and remove CSS classes with the `add_class()` and `remove_class()` methods. We will use these methods to connect the started state to the Start / Stop buttons.
The following code adds a event handler for the `Button.Pressed` event.
```python title="stopwatch04.py" hl_lines="11-15"
--8<-- "docs/examples/introduction/stopwatch04.py"
```
The `on_button_pressed` event handler is called when the user clicks a button. This method adds the "started" class when the "start" button was clicked, and removes the class when the "stop" button is clicked.
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/introduction/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter"}
```

View File

@@ -122,6 +122,14 @@ class ScreenStackError(ScreenError):
ReturnType = TypeVar("ReturnType")
class _NullFile:
def write(self, text: str) -> None:
pass
def flush(self) -> None:
pass
@rich.repr.auto
class App(Generic[ReturnType], DOMNode):
"""The base class for Textual Applications"""
@@ -168,7 +176,7 @@ class App(Generic[ReturnType], DOMNode):
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
self.console = Console(
file=(open(os.devnull, "wt") if self.is_headless else sys.__stdout__),
file=(_NullFile() if self.is_headless else sys.__stdout__),
markup=False,
highlight=False,
emoji=False,
@@ -584,9 +592,12 @@ class App(Generic[ReturnType], DOMNode):
driver = app._driver
assert driver is not None
for key in press:
print(f"press {key!r}")
driver.send_event(events.Key(self, key))
await asyncio.sleep(0.02)
if key == "_":
await asyncio.sleep(0.02)
else:
print(f"press {key!r}")
driver.send_event(events.Key(self, key))
await asyncio.sleep(0.02)
async def press_keys_task():
"""Press some keys in the background."""
@@ -1223,14 +1234,11 @@ class App(Generic[ReturnType], DOMNode):
Returns:
bool: True if the key was handled by a binding, otherwise False
"""
print("press", key)
try:
binding = self.bindings.get_key(key)
except NoBinding:
print("no binding")
return False
else:
print(binding)
await self.action(binding.action)
return True