Smarter delays - animator doesnt need to work during delay period

This commit is contained in:
Darren Burns
2022-08-30 15:51:26 +01:00
parent 779f49f858
commit 0455dacfdd
5 changed files with 86 additions and 17 deletions

View File

@@ -4,7 +4,7 @@ Screen {
#left_pane { #left_pane {
background: red; background: red;
width: 20 width: 20;
overflow: scroll scroll; overflow: scroll scroll;
} }

View File

@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from rich.console import RenderableType from rich.console import RenderableType
from textual import events from textual import events
@@ -32,6 +30,7 @@ class JustABox(App):
"opacity", "opacity",
value=0.0, value=0.0,
duration=2.0, duration=2.0,
delay=2.0,
on_complete=self.box.remove, on_complete=self.box.remove,
) )

View File

@@ -1,23 +1,25 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio import asyncio
import sys import sys
from typing import Any, Callable, TypeVar from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial
from typing import Any, Callable, TypeVar, TYPE_CHECKING
from . import _clock from . import _clock
from ._callback import invoke from ._callback import invoke
from ._easing import DEFAULT_EASING, EASING from ._easing import DEFAULT_EASING, EASING
from ._types import CallbackType
from .timer import Timer from .timer import Timer
from ._types import MessageTarget, CallbackType
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
from typing import Protocol, runtime_checkable from typing import Protocol, runtime_checkable
else: # pragma: no cover else: # pragma: no cover
from typing_extensions import Protocol, runtime_checkable from typing_extensions import Protocol, runtime_checkable
if TYPE_CHECKING:
from textual.app import App
EasingFunction = Callable[[float], float] EasingFunction = Callable[[float], float]
@@ -31,7 +33,6 @@ class Animatable(Protocol):
class Animation(ABC): class Animation(ABC):
on_complete: CallbackType | None = None on_complete: CallbackType | None = None
"""Callback to run after animation completes""" """Callback to run after animation completes"""
@@ -64,7 +65,6 @@ class SimpleAnimation(Animation):
on_complete: CallbackType | None = None on_complete: CallbackType | None = None
def __call__(self, time: float) -> bool: def __call__(self, time: float) -> bool:
if self.duration == 0: if self.duration == 0:
setattr(self.obj, self.attribute, self.final_value) setattr(self.obj, self.attribute, self.final_value)
return True return True
@@ -122,6 +122,7 @@ class BoundAnimator:
final_value: object = ..., final_value: object = ...,
duration: float | None = None, duration: float | None = None,
speed: float | None = None, speed: float | None = None,
delay: float = 0.0,
easing: EasingFunction | str = DEFAULT_EASING, easing: EasingFunction | str = DEFAULT_EASING,
on_complete: CallbackType | None = None, on_complete: CallbackType | None = None,
) -> None: ) -> None:
@@ -133,6 +134,7 @@ class BoundAnimator:
final_value=final_value, final_value=final_value,
duration=duration, duration=duration,
speed=speed, speed=speed,
delay=delay,
easing=easing_function, easing=easing_function,
on_complete=on_complete, on_complete=on_complete,
) )
@@ -141,7 +143,7 @@ class BoundAnimator:
class Animator: class Animator:
"""An object to manage updates to a given attribute over a period of time.""" """An object to manage updates to a given attribute over a period of time."""
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None: def __init__(self, target: App, frames_per_second: int = 60) -> None:
self._animations: dict[tuple[object, str], Animation] = {} self._animations: dict[tuple[object, str], Animation] = {}
self.target = target self.target = target
self._timer = Timer( self._timer = Timer(
@@ -179,6 +181,7 @@ class Animator:
duration: float | None = None, duration: float | None = None,
speed: float | None = None, speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING, easing: EasingFunction | str = DEFAULT_EASING,
delay: float = 0.0,
on_complete: CallbackType | None = None, on_complete: CallbackType | None = None,
) -> None: ) -> None:
"""Animate an attribute to a new value. """Animate an attribute to a new value.
@@ -191,8 +194,49 @@ class Animator:
duration (float | None, optional): The duration of the animation, or ``None`` to use speed. Defaults to ``None``. 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. speed (float | None, optional): The speed of the animation. Defaults to None.
easing (EasingFunction | str, optional): An easing function. Defaults to DEFAULT_EASING. easing (EasingFunction | str, optional): An easing function. Defaults to DEFAULT_EASING.
delay (float, optional): Number of seconds to delay the start of the animation by. Defaults to 0.
on_complete (CallbackType | None, optional): Callback to run after the animation completes.
""" """
animate_callback = partial(
self._animate,
obj,
attribute,
value,
final_value=final_value,
duration=duration,
speed=speed,
easing=easing,
on_complete=on_complete,
)
if delay:
self.target.set_timer(delay, animate_callback)
else:
animate_callback()
def _animate(
self,
obj: object,
attribute: str,
value: Any,
*,
final_value: object = ...,
duration: float | None = None,
speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING,
on_complete: CallbackType | None = None,
):
"""Animate an attribute to a new value.
Args:
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 ....
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.
on_complete (CallbackType | None, optional): Callback to run after the animation completes.
"""
if not hasattr(obj, attribute): if not hasattr(obj, attribute):
raise AttributeError( raise AttributeError(
f"Can't animate attribute {attribute!r} on {obj!r}; attribute does not exist" f"Can't animate attribute {attribute!r} on {obj!r}; attribute does not exist"
@@ -203,6 +247,7 @@ class Animator:
if final_value is ...: if final_value is ...:
final_value = value final_value = value
start_time = self._get_time() start_time = self._get_time()
animation_key = (id(obj), attribute) animation_key = (id(obj), attribute)
@@ -272,3 +317,29 @@ class Animator:
# N.B. We could remove this method and always call `self._timer.get_time()` internally, # N.B. We could remove this method and always call `self._timer.get_time()` internally,
# but it's handy to have in mocking situations # but it's handy to have in mocking situations
return _clock.get_time_no_wait() return _clock.get_time_no_wait()
if __name__ == "__main__":
async def run():
async def do(num):
print(num)
async def delayed(callable, *args):
await callable(*args)
async def delayed_long(callable, *args):
await asyncio.sleep(5)
await callable(*args)
tasks = []
for num in range(10):
if num == 2:
task = asyncio.create_task(delayed_long(do, num))
else:
task = asyncio.create_task(delayed(do, num))
tasks.append(task)
await asyncio.wait(tasks)
asyncio.run(run())

View File

@@ -1,13 +1,12 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING
from .._types import CallbackType
from ..geometry import Offset
from .._animator import Animation
from .scalar import ScalarOffset from .scalar import ScalarOffset
from .._animator import Animation
from .._animator import EasingFunction from .._animator import EasingFunction
from .._types import CallbackType
from ..geometry import Offset, clamp
if TYPE_CHECKING: if TYPE_CHECKING:
from ..widget import Widget from ..widget import Widget
@@ -52,8 +51,7 @@ class ScalarAnimation(Animation):
self.duration = duration self.duration = duration
def __call__(self, time: float) -> bool: def __call__(self, time: float) -> bool:
factor = clamp((time - self.start_time) / self.duration, 0.0, 1.0)
factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing(factor) eased_factor = self.easing(factor)
if eased_factor >= 1: if eased_factor >= 1:

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import os import os
from collections import defaultdict from collections import defaultdict
from functools import partial
from operator import itemgetter from operator import itemgetter
from pathlib import Path, PurePath from pathlib import Path, PurePath
from typing import Iterable, NamedTuple, cast from typing import Iterable, NamedTuple, cast