mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix and test for animator
This commit is contained in:
@@ -16,7 +16,7 @@ from ._types import MessageTarget
|
|||||||
|
|
||||||
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:
|
else: # pragma: no cover
|
||||||
from typing_extensions import Protocol, runtime_checkable
|
from typing_extensions import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
@@ -27,13 +27,13 @@ T = TypeVar("T")
|
|||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class Animatable(Protocol):
|
class Animatable(Protocol):
|
||||||
def blend(self: T, destination: T, factor: float) -> T:
|
def blend(self: T, destination: T, factor: float) -> T: # pragma: no cover
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class Animation(ABC):
|
class Animation(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __call__(self, time: float) -> bool:
|
def __call__(self, time: float) -> bool: # pragma: no cover
|
||||||
raise NotImplementedError("")
|
raise NotImplementedError("")
|
||||||
|
|
||||||
|
|
||||||
@@ -45,26 +45,19 @@ class SimpleAnimation(Animation):
|
|||||||
duration: float
|
duration: float
|
||||||
start_value: float | Animatable
|
start_value: float | Animatable
|
||||||
end_value: float | Animatable
|
end_value: float | Animatable
|
||||||
final_value: float | Animatable
|
final_value: object
|
||||||
easing: EasingFunction
|
easing: EasingFunction
|
||||||
|
|
||||||
def __call__(self, time: float) -> bool:
|
def __call__(self, time: float) -> bool:
|
||||||
def blend_float(start: float, end: float, factor: float) -> float:
|
|
||||||
return start + (end - start) * factor
|
|
||||||
|
|
||||||
AnimatableT = TypeVar("AnimatableT", bound=Animatable)
|
|
||||||
|
|
||||||
def blend(start: AnimatableT, end: AnimatableT, factor: float) -> AnimatableT:
|
|
||||||
return start.blend(end, factor)
|
|
||||||
|
|
||||||
if self.duration == 0:
|
if self.duration == 0:
|
||||||
value = self.end_value
|
value = self.final_value
|
||||||
else:
|
else:
|
||||||
factor = min(1.0, (time - self.start_time) / self.duration)
|
factor = min(1.0, (time - self.start_time) / self.duration)
|
||||||
eased_factor = self.easing(factor)
|
eased_factor = self.easing(factor)
|
||||||
|
|
||||||
if factor == 1.0:
|
if factor == 1.0:
|
||||||
value = self.end_value
|
value = self.final_value
|
||||||
elif isinstance(self.start_value, Animatable):
|
elif isinstance(self.start_value, Animatable):
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
self.end_value, Animatable
|
self.end_value, Animatable
|
||||||
@@ -88,7 +81,7 @@ class SimpleAnimation(Animation):
|
|||||||
+ (self.start_value - self.end_value) * eased_factor
|
+ (self.start_value - self.end_value) * eased_factor
|
||||||
)
|
)
|
||||||
setattr(self.obj, self.attribute, value)
|
setattr(self.obj, self.attribute, value)
|
||||||
return value == self.end_value
|
return value == self.final_value
|
||||||
|
|
||||||
|
|
||||||
class BoundAnimator:
|
class BoundAnimator:
|
||||||
@@ -101,7 +94,7 @@ class BoundAnimator:
|
|||||||
attribute: str,
|
attribute: str,
|
||||||
value: float,
|
value: float,
|
||||||
*,
|
*,
|
||||||
final_value: Any = ...,
|
final_value: object = ...,
|
||||||
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,
|
||||||
@@ -153,7 +146,7 @@ class Animator:
|
|||||||
attribute: str,
|
attribute: str,
|
||||||
value: Any,
|
value: Any,
|
||||||
*,
|
*,
|
||||||
final_value: Any = ...,
|
final_value: object = ...,
|
||||||
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,
|
||||||
|
|||||||
166
tests/test_animator.py
Normal file
166
tests/test_animator.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
from textual._animator import SimpleAnimation
|
||||||
|
|
||||||
|
|
||||||
|
class Animatable:
|
||||||
|
"""An animatable object."""
|
||||||
|
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def blend(self, destination: Animatable, factor: float) -> Animatable:
|
||||||
|
return Animatable(self.value + (destination.value - self.value) * factor)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AnimateTest:
|
||||||
|
"""An object to animate."""
|
||||||
|
|
||||||
|
foo: float | None = 0 # Plain float that may be set to None on final_value
|
||||||
|
bar: Animatable = Animatable(0) # A mock object supporting the animatable protocol
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_animation():
|
||||||
|
"""Test an animation from one float to another."""
|
||||||
|
|
||||||
|
# Thing that may be animated
|
||||||
|
animatable = AnimateTest()
|
||||||
|
|
||||||
|
# Fake wall-clock time
|
||||||
|
time = 100.0
|
||||||
|
|
||||||
|
# Object that does the animation
|
||||||
|
animation = SimpleAnimation(
|
||||||
|
animatable,
|
||||||
|
"foo",
|
||||||
|
time,
|
||||||
|
3.0,
|
||||||
|
start_value=20.0,
|
||||||
|
end_value=50.0,
|
||||||
|
final_value=None,
|
||||||
|
easing=lambda x: x,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert animation(time) is False
|
||||||
|
assert animatable.foo == 20.0
|
||||||
|
|
||||||
|
assert animation(time + 1.0) is False
|
||||||
|
assert animatable.foo == 30.0
|
||||||
|
|
||||||
|
assert animation(time + 2.0) is False
|
||||||
|
assert animatable.foo == 40.0
|
||||||
|
|
||||||
|
assert animation(time + 2.9) is False
|
||||||
|
assert pytest.approx(animatable.foo, 49.0)
|
||||||
|
|
||||||
|
assert animation(time + 3.0) is True # True to indicate animation is complete
|
||||||
|
assert animatable.foo is None # This is final_value
|
||||||
|
|
||||||
|
assert animation(time + 3.0) is True
|
||||||
|
assert animatable.foo is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_animation_duration_zero():
|
||||||
|
"""Test animation handles duration of 0."""
|
||||||
|
|
||||||
|
# Thing that may be animated
|
||||||
|
animatable = AnimateTest()
|
||||||
|
|
||||||
|
# Fake wall-clock time
|
||||||
|
time = 100.0
|
||||||
|
|
||||||
|
# Object that does the animation
|
||||||
|
animation = SimpleAnimation(
|
||||||
|
animatable,
|
||||||
|
"foo",
|
||||||
|
time,
|
||||||
|
0.0,
|
||||||
|
start_value=20.0,
|
||||||
|
end_value=50.0,
|
||||||
|
final_value=50.0,
|
||||||
|
easing=lambda x: x,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert animation(time) is True
|
||||||
|
assert animatable.foo == 50.0
|
||||||
|
|
||||||
|
assert animation(time + 1.0) is True
|
||||||
|
assert animatable.foo == 50.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_animation_reverse():
|
||||||
|
"""Test an animation from one float to another, where the end value is less than the start."""
|
||||||
|
|
||||||
|
# Thing that may be animated
|
||||||
|
animatable = AnimateTest()
|
||||||
|
|
||||||
|
# Fake wall-clock time
|
||||||
|
time = 100.0
|
||||||
|
|
||||||
|
# Object that does the animation
|
||||||
|
animation = SimpleAnimation(
|
||||||
|
animatable,
|
||||||
|
"foo",
|
||||||
|
time,
|
||||||
|
3.0,
|
||||||
|
start_value=50.0,
|
||||||
|
end_value=20.0,
|
||||||
|
final_value=20.0,
|
||||||
|
easing=lambda x: x,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert animation(time) is False
|
||||||
|
assert animatable.foo == 50.0
|
||||||
|
|
||||||
|
assert animation(time + 1.0) is False
|
||||||
|
assert animatable.foo == 40.0
|
||||||
|
|
||||||
|
assert animation(time + 2.0) is False
|
||||||
|
assert animatable.foo == 30.0
|
||||||
|
|
||||||
|
assert animation(time + 3.0) is True
|
||||||
|
assert animatable.foo == 20.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_animatable():
|
||||||
|
"""Test SimpleAnimation works with the Animatable protocol"""
|
||||||
|
|
||||||
|
animatable = AnimateTest()
|
||||||
|
|
||||||
|
# Fake wall-clock time
|
||||||
|
time = 100.0
|
||||||
|
|
||||||
|
# Object that does the animation
|
||||||
|
animation = SimpleAnimation(
|
||||||
|
animatable,
|
||||||
|
"bar",
|
||||||
|
time,
|
||||||
|
3.0,
|
||||||
|
start_value=Animatable(20.0),
|
||||||
|
end_value=Animatable(50.0),
|
||||||
|
final_value=Animatable(50.0),
|
||||||
|
easing=lambda x: x,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert animation(time) is False
|
||||||
|
assert animatable.bar.value == 20.0
|
||||||
|
|
||||||
|
assert animation(time + 1.0) is False
|
||||||
|
assert animatable.bar.value == 30.0
|
||||||
|
|
||||||
|
assert animation(time + 2.0) is False
|
||||||
|
assert animatable.bar.value == 40.0
|
||||||
|
|
||||||
|
assert animation(time + 2.9) is False
|
||||||
|
assert pytest.approx(animatable.bar.value, 49.0)
|
||||||
|
|
||||||
|
assert animation(time + 3.0) is True # True to indicate animation is complete
|
||||||
|
assert animatable.bar.value == 50.0
|
||||||
Reference in New Issue
Block a user