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