mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Smarter delays - animator doesnt need to work during delay period
This commit is contained in:
@@ -4,7 +4,7 @@ Screen {
|
|||||||
|
|
||||||
#left_pane {
|
#left_pane {
|
||||||
background: red;
|
background: red;
|
||||||
width: 20
|
width: 20;
|
||||||
overflow: scroll scroll;
|
overflow: scroll scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user