mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' of github.com:Textualize/textual into key-aliases
This commit is contained in:
19
docs/examples/guide/animator/animation01.py
Normal file
19
docs/examples/guide/animator/animation01.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class AnimationApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
self.box = Static("Hello, World!")
|
||||
self.box.styles.background = "red"
|
||||
self.box.styles.color = "black"
|
||||
self.box.styles.padding = (1, 2)
|
||||
yield self.box
|
||||
|
||||
def on_mount(self):
|
||||
self.box.styles.animate("opacity", value=0.0, duration=2.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = AnimationApp()
|
||||
app.run()
|
||||
16
docs/examples/guide/animator/animation01_static.py
Normal file
16
docs/examples/guide/animator/animation01_static.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class AnimationApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
self.box = Static("Hello, World!")
|
||||
self.box.styles.background = "red"
|
||||
self.box.styles.color = "black"
|
||||
self.box.styles.padding = (1, 2)
|
||||
yield self.box
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = AnimationApp()
|
||||
app.run()
|
||||
80
docs/guide/animation.md
Normal file
80
docs/guide/animation.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Animation
|
||||
|
||||
Ths chapter discusses how to use Textual's animation system.
|
||||
|
||||
|
||||
## Animating styles
|
||||
|
||||
Textual's animator can change an attribute from one value to another in fixed increments over a period of time. You can apply this to [styles](styles.md) such `offset` to move widgets around the screen, and `opacity` to create fading effects.
|
||||
|
||||
Apps and widgets both have an [animate][textual.app.App.animate] method which will animate properties on those objects. Additionally, `styles` objects have an identical `animate` method which will animate styles.
|
||||
|
||||
Let's look at an example of how we can animate the opacity of a widget to make it fade out.
|
||||
The following example app contains a single `Static` widget which is immediately animated to an opacity of `0.0` (making it invisible) over a duration of two seconds.
|
||||
|
||||
```python hl_lines="14"
|
||||
--8<-- "docs/examples/guide/animator/animation01.py"
|
||||
```
|
||||
|
||||
The animator updates the value of the `opacity` attribute on the `styles` object in small increments over two seconds. Here's what the output will look like after each half a second.
|
||||
|
||||
|
||||
=== "After 0s"
|
||||
|
||||
```{.textual path="docs/examples/guide/animator/animation01_static.py"}
|
||||
```
|
||||
|
||||
=== "After 0.5s"
|
||||
|
||||
```{.textual path="docs/examples/guide/animator/animation01.py" press="wait:500"}
|
||||
```
|
||||
|
||||
|
||||
=== "After 1s"
|
||||
|
||||
```{.textual path="docs/examples/guide/animator/animation01.py" press="wait:1000"}
|
||||
```
|
||||
|
||||
=== "After 1.5s"
|
||||
|
||||
```{.textual path="docs/examples/guide/animator/animation01.py" press="wait:1500"}
|
||||
```
|
||||
|
||||
=== "After 2s"
|
||||
|
||||
```{.textual path="docs/examples/guide/animator/animation01.py" press="wait:2000"}
|
||||
```
|
||||
|
||||
## Duration and Speed
|
||||
|
||||
When requesting an animation you can specify a *duration* or *speed*.
|
||||
The duration is how long the animation should take in seconds. The speed is how many units a value should change in one second.
|
||||
For instance, if you animate a value at 0 to 10 with a speed of 2, it will complete in 5 seconds.
|
||||
|
||||
## Easing functions
|
||||
|
||||
The easing function determines the journey a value takes on its way to the target value.
|
||||
It could move at a constant pace, or it might start off slow then accelerate towards its final value.
|
||||
Textual supports a number of [easing functions](https://easings.net/). Run the following from the command prompt to preview them.
|
||||
|
||||
```bash
|
||||
textual easing
|
||||
```
|
||||
|
||||
You can specify which easing method to use via the `easing` parameter on the `animate` method. The default easing method is `"in_out_cubic"` which accelerates and then decelerates to produce a pleasing organic motion.
|
||||
|
||||
!!! note
|
||||
|
||||
The `textual easing` preview requires the `dev` extras to be installed (using `pip install textual[dev]`).
|
||||
|
||||
|
||||
## Completion callbacks
|
||||
|
||||
You can pass an callable to the animator via the `on_complete` parameter. Textual will run the callable when the animation has completed.
|
||||
|
||||
## Delaying animations
|
||||
|
||||
You can delay the start of an animation with the `delay` parameter of the `animate` method.
|
||||
This parameter accepts a `float` value representing the number of seconds to delay the animation by.
|
||||
For example, `self.box.styles.animate("opacity", value=0.0, duration=2.0, delay=5.0)` delays the start of the animation by five seconds,
|
||||
meaning the animation will start after 5 seconds and complete 2 seconds after that.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Animator
|
||||
|
||||
TODO: Animator docs
|
||||
@@ -301,7 +301,7 @@ We'll also add a slight tint using `tint: magenta 40%;` to draw attention to it.
|
||||
|
||||
=== "grid_layout5_col_span.py"
|
||||
|
||||
```python
|
||||
```python
|
||||
--8<-- "docs/examples/guide/layout/grid_layout5_col_span.py"
|
||||
```
|
||||
|
||||
@@ -544,7 +544,7 @@ The example below shows how an advanced layout can be built by combining the var
|
||||
|
||||
=== "combining_layouts.css"
|
||||
|
||||
```sass
|
||||
```sass
|
||||
--8<-- "docs/examples/guide/layout/combining_layouts.css"
|
||||
```
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ The example below populates a table with CSV data.
|
||||
|
||||
## See Also
|
||||
|
||||
* [Table][textual.widgets.DataTable] code reference
|
||||
* [DataTable][textual.widgets.DataTable] code reference
|
||||
|
||||
@@ -20,7 +20,7 @@ nav:
|
||||
- "guide/actions.md"
|
||||
- "guide/reactivity.md"
|
||||
- "guide/widgets.md"
|
||||
- "guide/animator.md"
|
||||
- "guide/animation.md"
|
||||
- "guide/screens.md"
|
||||
- How to:
|
||||
- "how-to/index.md"
|
||||
@@ -95,7 +95,7 @@ nav:
|
||||
- "widgets/input.md"
|
||||
- "widgets/static.md"
|
||||
- "widgets/tree_control.md"
|
||||
- Reference:
|
||||
- Reference:
|
||||
- "reference/app.md"
|
||||
- "reference/button.md"
|
||||
- "reference/color.md"
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.widgets import Static, Footer
|
||||
from textual.widgets import Static, Footer, Header
|
||||
|
||||
|
||||
class JustABox(App):
|
||||
@@ -12,6 +12,7 @@ class JustABox(App):
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Static("Hello, world!", id="box1")
|
||||
yield Footer()
|
||||
|
||||
|
||||
@@ -126,6 +126,19 @@ class BoundAnimator:
|
||||
easing: EasingFunction | str = DEFAULT_EASING,
|
||||
on_complete: CallbackType | None = None,
|
||||
) -> None:
|
||||
"""Animate an attribute.
|
||||
|
||||
Args:
|
||||
attribute (str): Name of the attribute to animate.
|
||||
value (float | Animatable): The value to animate to.
|
||||
final_value (object, optional): The final value of the animation. Defaults to `value` if not set.
|
||||
duration (float | None, optional): The duration of the animate. Defaults to None.
|
||||
speed (float | None, optional): The speed of the animation. Defaults to None.
|
||||
delay (float, optional): A delay (in seconds) before the animation starts. Defaults to 0.0.
|
||||
easing (EasingFunction | str, optional): An easing method. Defaults to "in_out_cubic".
|
||||
on_complete (CallbackType | None, optional): A callable to invoke when the animation is finished. Defaults to None.
|
||||
|
||||
"""
|
||||
easing_function = EASING[easing] if isinstance(easing, str) else easing
|
||||
return self._animator.animate(
|
||||
self._obj,
|
||||
@@ -191,7 +204,7 @@ class Animator:
|
||||
obj (object): The object containing the attribute.
|
||||
attribute (str): The name of the attribute.
|
||||
value (Any): The destination value of the attribute.
|
||||
final_value (Any, optional): The final value, or ellipsis if it is the same as ``value``. Defaults to ....
|
||||
final_value (Any, optional): The final value, or ellipsis if it is the same as ``value``. Defaults to Ellipsis/
|
||||
duration (float | None, optional): The duration of the animation, or ``None`` to use speed. Defaults to ``None``.
|
||||
speed (float | None, optional): The speed of the animation. Defaults to None.
|
||||
easing (EasingFunction | str, optional): An easing function. Defaults to DEFAULT_EASING.
|
||||
|
||||
@@ -31,7 +31,7 @@ from rich.segment import Segment, Segments
|
||||
from rich.traceback import Traceback
|
||||
|
||||
from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages
|
||||
from ._animator import Animator
|
||||
from ._animator import Animator, DEFAULT_EASING, Animatable, EasingFunction
|
||||
from ._callback import invoke
|
||||
from ._context import active_app
|
||||
from ._event_broker import NoHandler, extract_handler_actions
|
||||
@@ -183,7 +183,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
self._action_targets = {"app", "screen"}
|
||||
self._animator = Animator(self)
|
||||
self.animate = self._animator.bind(self)
|
||||
self._animate = self._animator.bind(self)
|
||||
self.mouse_position = Offset(0, 0)
|
||||
if title is None:
|
||||
self._title = f"{self.__class__.__name__}"
|
||||
@@ -230,6 +230,42 @@ class App(Generic[ReturnType], DOMNode):
|
||||
sub_title: Reactive[str] = Reactive("")
|
||||
dark: Reactive[bool] = Reactive(True)
|
||||
|
||||
def animate(
|
||||
self,
|
||||
attribute: str,
|
||||
value: float | Animatable,
|
||||
*,
|
||||
final_value: object = ...,
|
||||
duration: float | None = None,
|
||||
speed: float | None = None,
|
||||
delay: float = 0.0,
|
||||
easing: EasingFunction | str = DEFAULT_EASING,
|
||||
on_complete: CallbackType | None = None,
|
||||
) -> None:
|
||||
"""Animate an attribute.
|
||||
|
||||
Args:
|
||||
attribute (str): Name of the attribute to animate.
|
||||
value (float | Animatable): The value to animate to.
|
||||
final_value (object, optional): The final value of the animation. Defaults to `value` if not set.
|
||||
duration (float | None, optional): The duration of the animate. Defaults to None.
|
||||
speed (float | None, optional): The speed of the animation. Defaults to None.
|
||||
delay (float, optional): A delay (in seconds) before the animation starts. Defaults to 0.0.
|
||||
easing (EasingFunction | str, optional): An easing method. Defaults to "in_out_cubic".
|
||||
on_complete (CallbackType | None, optional): A callable to invoke when the animation is finished. Defaults to None.
|
||||
|
||||
"""
|
||||
self._animate(
|
||||
attribute,
|
||||
value,
|
||||
final_value=final_value,
|
||||
duration=duration,
|
||||
speed=speed,
|
||||
delay=delay,
|
||||
easing=easing,
|
||||
on_complete=on_complete,
|
||||
)
|
||||
|
||||
@property
|
||||
def devtools_enabled(self) -> bool:
|
||||
"""Check if devtools are enabled.
|
||||
@@ -655,8 +691,12 @@ class App(Generic[ReturnType], DOMNode):
|
||||
await asyncio.sleep(0.01)
|
||||
for key in press:
|
||||
if key == "_":
|
||||
print("(pause)")
|
||||
print("(pause 50ms)")
|
||||
await asyncio.sleep(0.05)
|
||||
elif key.startswith("wait:"):
|
||||
_, wait_ms = key.split(":")
|
||||
print(f"(pause {wait_ms}ms)")
|
||||
await asyncio.sleep(float(wait_ms) / 1000)
|
||||
else:
|
||||
print(f"press {key!r}")
|
||||
driver.send_event(
|
||||
|
||||
@@ -11,7 +11,7 @@ import rich.repr
|
||||
from rich.style import Style
|
||||
|
||||
from .._types import CallbackType
|
||||
from .._animator import Animation, EasingFunction, BoundAnimator
|
||||
from .._animator import BoundAnimator, DEFAULT_EASING, Animatable, EasingFunction
|
||||
from ..color import Color
|
||||
from ..geometry import Offset, Spacing
|
||||
from ._style_properties import (
|
||||
@@ -889,22 +889,44 @@ class RenderStyles(StylesBase):
|
||||
assert self.node is not None
|
||||
return self.node.rich_style
|
||||
|
||||
@property
|
||||
def animate(self) -> BoundAnimator:
|
||||
"""Get an animator to animate style.
|
||||
def animate(
|
||||
self,
|
||||
attribute: str,
|
||||
value: float | Animatable,
|
||||
*,
|
||||
final_value: object = ...,
|
||||
duration: float | None = None,
|
||||
speed: float | None = None,
|
||||
delay: float = 0.0,
|
||||
easing: EasingFunction | str = DEFAULT_EASING,
|
||||
on_complete: CallbackType | None = None,
|
||||
) -> None:
|
||||
"""Animate an attribute.
|
||||
|
||||
Example:
|
||||
```python
|
||||
self.animate("brightness", 0.5)
|
||||
```
|
||||
Args:
|
||||
attribute (str): Name of the attribute to animate.
|
||||
value (float | Animatable): The value to animate to.
|
||||
final_value (object, optional): The final value of the animation. Defaults to `value` if not set.
|
||||
duration (float | None, optional): The duration of the animate. Defaults to None.
|
||||
speed (float | None, optional): The speed of the animation. Defaults to None.
|
||||
delay (float, optional): A delay (in seconds) before the animation starts. Defaults to 0.0.
|
||||
easing (EasingFunction | str, optional): An easing method. Defaults to "in_out_cubic".
|
||||
on_complete (CallbackType | None, optional): A callable to invoke when the animation is finished. Defaults to None.
|
||||
|
||||
Returns:
|
||||
BoundAnimator: An animator bound to this widget.
|
||||
"""
|
||||
if self._animate is None:
|
||||
self._animate = self.node.app.animator.bind(self)
|
||||
assert self._animate is not None
|
||||
return self._animate
|
||||
self._animate(
|
||||
attribute,
|
||||
value,
|
||||
final_value=final_value,
|
||||
duration=duration,
|
||||
speed=speed,
|
||||
delay=delay,
|
||||
easing=easing,
|
||||
on_complete=on_complete,
|
||||
)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
for rule_name in RULE_NAMES:
|
||||
|
||||
@@ -22,7 +22,7 @@ from rich.style import Style
|
||||
from rich.text import Text
|
||||
|
||||
from . import errors, events, messages
|
||||
from ._animator import BoundAnimator
|
||||
from ._animator import BoundAnimator, DEFAULT_EASING, Animatable, EasingFunction
|
||||
from ._arrange import DockArrangeResult, arrange
|
||||
from ._context import active_app
|
||||
from ._layout import Layout
|
||||
@@ -36,6 +36,7 @@ from .dom import DOMNode, NoScreen
|
||||
from .geometry import Offset, Region, Size, Spacing, clamp
|
||||
from .layouts.vertical import VerticalLayout
|
||||
from .message import Message
|
||||
from .messages import CallbackType
|
||||
from .reactive import Reactive
|
||||
from .render import measure
|
||||
|
||||
@@ -816,22 +817,44 @@ class Widget(DOMNode):
|
||||
"""
|
||||
return active_app.get().console
|
||||
|
||||
@property
|
||||
def animate(self) -> BoundAnimator:
|
||||
"""Get an animator to animate attributes on this widget.
|
||||
def animate(
|
||||
self,
|
||||
attribute: str,
|
||||
value: float | Animatable,
|
||||
*,
|
||||
final_value: object = ...,
|
||||
duration: float | None = None,
|
||||
speed: float | None = None,
|
||||
delay: float = 0.0,
|
||||
easing: EasingFunction | str = DEFAULT_EASING,
|
||||
on_complete: CallbackType | None = None,
|
||||
) -> None:
|
||||
"""Animate an attribute.
|
||||
|
||||
Example:
|
||||
```python
|
||||
self.animate("brightness", 0.5)
|
||||
```
|
||||
Args:
|
||||
attribute (str): Name of the attribute to animate.
|
||||
value (float | Animatable): The value to animate to.
|
||||
final_value (object, optional): The final value of the animation. Defaults to `value` if not set.
|
||||
duration (float | None, optional): The duration of the animate. Defaults to None.
|
||||
speed (float | None, optional): The speed of the animation. Defaults to None.
|
||||
delay (float, optional): A delay (in seconds) before the animation starts. Defaults to 0.0.
|
||||
easing (EasingFunction | str, optional): An easing method. Defaults to "in_out_cubic".
|
||||
on_complete (CallbackType | None, optional): A callable to invoke when the animation is finished. Defaults to None.
|
||||
|
||||
Returns:
|
||||
BoundAnimator: An animator bound to this widget.
|
||||
"""
|
||||
if self._animate is None:
|
||||
self._animate = self.app.animator.bind(self)
|
||||
assert self._animate is not None
|
||||
return self._animate
|
||||
self._animate(
|
||||
attribute,
|
||||
value,
|
||||
final_value=final_value,
|
||||
duration=duration,
|
||||
speed=speed,
|
||||
delay=delay,
|
||||
easing=easing,
|
||||
on_complete=on_complete,
|
||||
)
|
||||
|
||||
@property
|
||||
def _layout(self) -> Layout:
|
||||
|
||||
@@ -54,6 +54,7 @@ class HeaderTitle(Widget):
|
||||
HeaderTitle {
|
||||
content-align: center middle;
|
||||
width: 100%;
|
||||
margin-right: 10;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -69,7 +70,11 @@ class HeaderTitle(Widget):
|
||||
|
||||
|
||||
class Header(Widget):
|
||||
"""A header widget with icon and clock."""
|
||||
"""A header widget with icon and clock.
|
||||
|
||||
Args:
|
||||
show_clock (bool, optional): True if the clock should be shown on the right of the header.
|
||||
"""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Header {
|
||||
@@ -88,10 +93,27 @@ class Header(Widget):
|
||||
|
||||
DEFAULT_CLASSES = "-tall"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
show_clock: bool = False,
|
||||
*,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
):
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
self.show_clock = show_clock
|
||||
|
||||
def compose(self):
|
||||
yield HeaderIcon()
|
||||
yield HeaderTitle()
|
||||
if self.show_clock:
|
||||
yield HeaderClock()
|
||||
|
||||
def watch_tall(self, tall: bool) -> None:
|
||||
self.set_class(tall, "-tall")
|
||||
|
||||
async def on_click(self, event):
|
||||
def on_click(self):
|
||||
self.toggle_class("-tall")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
@@ -103,8 +125,3 @@ class Header(Widget):
|
||||
|
||||
watch(self.app, "title", set_title)
|
||||
watch(self.app, "sub_title", set_sub_title)
|
||||
|
||||
def compose(self):
|
||||
yield HeaderIcon()
|
||||
yield HeaderTitle()
|
||||
yield HeaderClock()
|
||||
|
||||
Reference in New Issue
Block a user