mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
introduction text
This commit is contained in:
@@ -69,13 +69,7 @@ class Stopwatch(Static):
|
|||||||
|
|
||||||
|
|
||||||
class StopwatchApp(App):
|
class StopwatchApp(App):
|
||||||
"""Manage the timers."""
|
"""A Textual app to manage stopwatches."""
|
||||||
|
|
||||||
def on_load(self) -> None:
|
|
||||||
"""Called when the app first loads."""
|
|
||||||
self.bind("a", "add_stopwatch", description="Add")
|
|
||||||
self.bind("r", "remove_stopwatch", description="Remove")
|
|
||||||
self.bind("d", "toggle_dark", description="Dark mode")
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Called to ad widgets to the app."""
|
"""Called to ad widgets to the app."""
|
||||||
@@ -83,6 +77,12 @@ class StopwatchApp(App):
|
|||||||
yield Footer()
|
yield Footer()
|
||||||
yield Container(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")
|
yield Container(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")
|
||||||
|
|
||||||
|
def on_load(self) -> None:
|
||||||
|
"""Called when the app first loads."""
|
||||||
|
self.bind("d", "toggle_dark", description="Dark mode")
|
||||||
|
self.bind("a", "add_stopwatch", description="Add")
|
||||||
|
self.bind("r", "remove_stopwatch", description="Remove")
|
||||||
|
|
||||||
def action_add_stopwatch(self) -> None:
|
def action_add_stopwatch(self) -> None:
|
||||||
"""An action to add a timer."""
|
"""An action to add a timer."""
|
||||||
new_stopwatch = Stopwatch()
|
new_stopwatch = Stopwatch()
|
||||||
@@ -91,7 +91,7 @@ class StopwatchApp(App):
|
|||||||
|
|
||||||
def action_remove_stopwatch(self) -> None:
|
def action_remove_stopwatch(self) -> None:
|
||||||
"""Called to remove a timer."""
|
"""Called to remove a timer."""
|
||||||
timers = self.query("#timers Stopwatch")
|
timers = self.query("Stopwatch")
|
||||||
if timers:
|
if timers:
|
||||||
timers.last().remove()
|
timers.last().remove()
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class TimeDisplay(Static):
|
|||||||
"""A widget to display elapsed time."""
|
"""A widget to display elapsed time."""
|
||||||
|
|
||||||
start_time = Reactive(monotonic)
|
start_time = Reactive(monotonic)
|
||||||
time = Reactive(0.0)
|
time = Reactive.init(0.0)
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Event handler called when widget is added to the app."""
|
"""Event handler called when widget is added to the app."""
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class TimeDisplay(Static):
|
|||||||
"""A widget to display elapsed time."""
|
"""A widget to display elapsed time."""
|
||||||
|
|
||||||
start_time = Reactive(monotonic)
|
start_time = Reactive(monotonic)
|
||||||
time = Reactive(0.0)
|
time = Reactive.init(0.0)
|
||||||
total = Reactive(0.0)
|
total = Reactive(0.0)
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -328,12 +328,17 @@ Here we have created two reactive attributes: `start_time` and `time`. These att
|
|||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
||||||
`Reactive` is an example of a Python _descriptor_, which allows you to dynamically create properties.
|
The `monotonic` function in this example is imported from the standard library `time` module. It is similar to `time.time` but won't go backwards if the system clock is changed.
|
||||||
|
|
||||||
The first argument to `Reactive` may be a default value or a callable that returns the default value. In the example, the default for `start_time` is `monotonic` which is a function that returns the time. When `TimeDisplay` is mounted, the `start_time` attribute will be assigned the result of `monotonic()`.
|
The first argument to `Reactive` may be a default value or a callable that returns the default value. In the example, the default for `start_time` is `monotonic`. When `TimeDisplay` is mounted, the `start_time` attribute will be assigned the result of `monotonic()`.
|
||||||
|
|
||||||
The `time` attribute has a simple float as the default value, so `self.time` will be `0` on start.
|
The `time` attribute has a simple float as the default value, so `self.time` will be `0` on start.
|
||||||
|
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
The `time` attribute is created with `Reactive.init` which calls watch methods when the widget is mounted. See below for an explanation of watch methods.
|
||||||
|
|
||||||
In the `on_mount` handler method, the call to `set_interval` creates a timer object which calls `self.update_time` sixty times a second. This `update_time` method calculates the time elapsed since the widget started and assigns it to `self.time`. Which brings us to one of Reactive's super-powers.
|
In the `on_mount` handler method, the call to `set_interval` creates a timer object which calls `self.update_time` sixty times a second. This `update_time` method calculates the time elapsed since the widget started and assigns it to `self.time`. Which brings us to one of Reactive's super-powers.
|
||||||
|
|
||||||
If you implement a method that begins with `watch_` followed by the name of a reactive attribute (making it a _watch method_), that method will be called when the attribute is modified.
|
If you implement a method that begins with `watch_` followed by the name of a reactive attribute (making it a _watch method_), that method will be called when the attribute is modified.
|
||||||
@@ -393,3 +398,32 @@ If you run stopwatch06.py you will be able to use the stopwatches independently.
|
|||||||
```
|
```
|
||||||
|
|
||||||
The only remaining feature of the Stopwatch app let to implement is the ability to add and remove timers.
|
The only remaining feature of the Stopwatch app let to implement is the ability to add and remove timers.
|
||||||
|
|
||||||
|
## Dynamic widgets
|
||||||
|
|
||||||
|
It's convenient to build a user interface with the `compose` method. We may also want to add or remove widgets while the app is running.
|
||||||
|
|
||||||
|
To add a new child widget call `mount()` on the parent. To remove a widget, call it's `remove()` method.
|
||||||
|
|
||||||
|
Let's use these to implement adding and removing stopwatches to our app.
|
||||||
|
|
||||||
|
```python title="stopwatch.py" hl_lines="83-84 86-90 92-96"
|
||||||
|
--8<-- "docs/examples/introduction/stopwatch.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
We've added two new actions: `action_add_stopwatch` to add a new stopwatch, and `action_remove_stopwatch`) to remove the last stopwatch. The `on_load` handler binds these actions to the ++a++ and ++r++ keys.
|
||||||
|
|
||||||
|
The `action_add_stopwatch` method creates and mounts a new `Stopwatch` instance. Note the call to `query_one` with a CSS selector of `"#timers"` which gets the timer's container via its ID (assigned in `compose`). Once mounted, the new Stopwatch will appear in the terminal. That last line in `action_add_stopwatch` calls `scroll_visible` which will scroll the container to make the new Stopwatch visible (if necessary).
|
||||||
|
|
||||||
|
The `action_remove_stopwatch` calls `query` with a CSS selector of `"Stopwatch"` which gets all the `Stopwatch` widgets. If there are stopwatches then the action calls `last()` to get the last stopwatch, and `remove()` to remove it.
|
||||||
|
|
||||||
|
If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and remove a stopwatch with ++r++.
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/introduction/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,_,_,_,_,tab,_"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## What next?
|
||||||
|
|
||||||
|
Congratulations on building your first Textual application! This introduction 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.
|
||||||
|
|||||||
@@ -940,12 +940,12 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
is_renderable(renderable) for renderable in renderables
|
is_renderable(renderable) for renderable in renderables
|
||||||
), "Can only call panic with strings or Rich renderables"
|
), "Can only call panic with strings or Rich renderables"
|
||||||
|
|
||||||
prerendered = [
|
pre_rendered = [
|
||||||
Segments(self.console.render(renderable, self.console.options))
|
Segments(self.console.render(renderable, self.console.options))
|
||||||
for renderable in renderables
|
for renderable in renderables
|
||||||
]
|
]
|
||||||
|
|
||||||
self._exit_renderables.extend(prerendered)
|
self._exit_renderables.extend(pre_rendered)
|
||||||
self.close_messages_no_wait()
|
self.close_messages_no_wait()
|
||||||
|
|
||||||
def on_exception(self, error: Exception) -> None:
|
def on_exception(self, error: Exception) -> None:
|
||||||
|
|||||||
@@ -320,13 +320,16 @@ class Stylesheet:
|
|||||||
animate (bool, optional): Animate changed rules. Defaults to ``False``.
|
animate (bool, optional): Animate changed rules. Defaults to ``False``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO: Need to optimize to make applying stylesheet more efficient
|
||||||
|
# I think we can pre-calculate which rules may be applicable to a given node
|
||||||
|
|
||||||
# Dictionary of rule attribute names e.g. "text_background" to list of tuples.
|
# Dictionary of rule attribute names e.g. "text_background" to list of tuples.
|
||||||
# The tuples contain the rule specificity, and the value for that rule.
|
# The tuples contain the rule specificity, and the value for that rule.
|
||||||
# We can use this to determine, for a given rule, whether we should apply it
|
# We can use this to determine, for a given rule, whether we should apply it
|
||||||
# or not by examining the specificity. If we have two rules for the
|
# or not by examining the specificity. If we have two rules for the
|
||||||
# same attribute, then we can choose the most specific rule and use that.
|
# same attribute, then we can choose the most specific rule and use that.
|
||||||
rule_attributes: dict[str, list[tuple[Specificity6, object]]]
|
rule_attributes: dict[str, list[tuple[Specificity6, object]]]
|
||||||
rule_attributes = defaultdict(list)
|
rule_attributes = {}
|
||||||
|
|
||||||
_check_rule = self._check_rule
|
_check_rule = self._check_rule
|
||||||
|
|
||||||
@@ -338,7 +341,9 @@ class Stylesheet:
|
|||||||
for key, rule_specificity, value in rule.styles.extract_rules(
|
for key, rule_specificity, value in rule.styles.extract_rules(
|
||||||
base_specificity, is_default_rules, tie_breaker
|
base_specificity, is_default_rules, tie_breaker
|
||||||
):
|
):
|
||||||
rule_attributes[key].append((rule_specificity, value))
|
rule_attributes.setdefault(key, []).append(
|
||||||
|
(rule_specificity, value)
|
||||||
|
)
|
||||||
|
|
||||||
# For each rule declared for this node, keep only the most specific one
|
# For each rule declared for this node, keep only the most specific one
|
||||||
get_first_item = itemgetter(0)
|
get_first_item = itemgetter(0)
|
||||||
@@ -433,11 +438,13 @@ class Stylesheet:
|
|||||||
apply = self.apply
|
apply = self.apply
|
||||||
for node in root.walk_children():
|
for node in root.walk_children():
|
||||||
apply(node, animate=animate)
|
apply(node, animate=animate)
|
||||||
if isinstance(node, Widget):
|
if isinstance(node, Widget) and node.is_scrollable:
|
||||||
if node.show_vertical_scrollbar:
|
if node.show_vertical_scrollbar:
|
||||||
apply(node.vertical_scrollbar)
|
apply(node.vertical_scrollbar)
|
||||||
if node.show_horizontal_scrollbar:
|
if node.show_horizontal_scrollbar:
|
||||||
apply(node.horizontal_scrollbar)
|
apply(node.horizontal_scrollbar)
|
||||||
|
if node.show_horizontal_scrollbar and node.show_vertical_scrollbar:
|
||||||
|
apply(node.scrollbar_corner)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from ._callback import invoke
|
|||||||
from .geometry import Offset, Region, Size
|
from .geometry import Offset, Region, Size
|
||||||
from ._compositor import Compositor, MapGeometry
|
from ._compositor import Compositor, MapGeometry
|
||||||
from .messages import CallbackType
|
from .messages import CallbackType
|
||||||
|
from ._profile import timer
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
from .renderables.blank import Blank
|
from .renderables.blank import Blank
|
||||||
from ._timer import Timer
|
from ._timer import Timer
|
||||||
|
|||||||
@@ -232,14 +232,14 @@ class ScrollBar(Widget):
|
|||||||
style=scrollbar_style,
|
style=scrollbar_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hide(self, event: events.Hide) -> None:
|
def _on_hide(self, event: events.Hide) -> None:
|
||||||
if self.grabbed:
|
if self.grabbed:
|
||||||
self.release_mouse()
|
self.release_mouse()
|
||||||
|
|
||||||
def on_enter(self, event: events.Enter) -> None:
|
def _on_enter(self, event: events.Enter) -> None:
|
||||||
self.mouse_over = True
|
self.mouse_over = True
|
||||||
|
|
||||||
def on_leave(self, event: events.Leave) -> None:
|
def _on_leave(self, event: events.Leave) -> None:
|
||||||
self.mouse_over = False
|
self.mouse_over = False
|
||||||
|
|
||||||
async def action_scroll_down(self) -> None:
|
async def action_scroll_down(self) -> None:
|
||||||
@@ -254,18 +254,18 @@ class ScrollBar(Widget):
|
|||||||
def action_released(self) -> None:
|
def action_released(self) -> None:
|
||||||
self.capture_mouse(False)
|
self.capture_mouse(False)
|
||||||
|
|
||||||
async def on_mouse_up(self, event: events.MouseUp) -> None:
|
async def _on_mouse_up(self, event: events.MouseUp) -> None:
|
||||||
if self.grabbed:
|
if self.grabbed:
|
||||||
self.release_mouse()
|
self.release_mouse()
|
||||||
|
|
||||||
def on_mouse_capture(self, event: events.MouseCapture) -> None:
|
def _on_mouse_capture(self, event: events.MouseCapture) -> None:
|
||||||
self.grabbed = event.mouse_position
|
self.grabbed = event.mouse_position
|
||||||
self.grabbed_position = self.position
|
self.grabbed_position = self.position
|
||||||
|
|
||||||
def on_mouse_release(self, event: events.MouseRelease) -> None:
|
def _on_mouse_release(self, event: events.MouseRelease) -> None:
|
||||||
self.grabbed = None
|
self.grabbed = None
|
||||||
|
|
||||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
||||||
if self.grabbed and self.window_size:
|
if self.grabbed and self.window_size:
|
||||||
x: float | None = None
|
x: float | None = None
|
||||||
y: float | None = None
|
y: float | None = None
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import (
|
|||||||
ClassVar,
|
ClassVar,
|
||||||
Collection,
|
Collection,
|
||||||
Iterable,
|
Iterable,
|
||||||
|
Iterator,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -626,7 +627,7 @@ class Widget(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if this widget may be scrolled.
|
bool: True if this widget may be scrolled.
|
||||||
"""
|
"""
|
||||||
return self.is_container
|
return self.styles.layout is not None or bool(self.children)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def layer(self) -> str:
|
def layer(self) -> str:
|
||||||
@@ -699,7 +700,6 @@ class Widget(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if the scroll position changed, otherwise False.
|
bool: True if the scroll position changed, otherwise False.
|
||||||
"""
|
"""
|
||||||
self.log(self, x, y, verbosity=0)
|
|
||||||
scrolled_x = scrolled_y = False
|
scrolled_x = scrolled_y = False
|
||||||
if animate:
|
if animate:
|
||||||
# TODO: configure animation speed
|
# TODO: configure animation speed
|
||||||
@@ -1035,7 +1035,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
||||||
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
||||||
# self.refresh(layout=True)
|
self.refresh(layout=True)
|
||||||
self.scroll_to(self.scroll_x, self.scroll_y)
|
self.scroll_to(self.scroll_x, self.scroll_y)
|
||||||
# self.call_later(self.scroll_to, self.scroll_x, self.scroll_y)
|
# self.call_later(self.scroll_to, self.scroll_x, self.scroll_y)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user