99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from textual._animator import Animation, EasingFunction
|
|
from textual._types import AnimationLevel, CallbackType
|
|
from textual.css.scalar import Scalar, ScalarOffset
|
|
|
|
if TYPE_CHECKING:
|
|
from textual.css.styles import StylesBase
|
|
from textual.widget import Widget
|
|
|
|
|
|
class ScalarAnimation(Animation):
|
|
def __init__(
|
|
self,
|
|
widget: Widget,
|
|
styles: StylesBase,
|
|
start_time: float,
|
|
attribute: str,
|
|
value: ScalarOffset | Scalar,
|
|
duration: float | None,
|
|
speed: float | None,
|
|
easing: EasingFunction,
|
|
on_complete: CallbackType | None = None,
|
|
level: AnimationLevel = "full",
|
|
):
|
|
assert (
|
|
speed is not None or duration is not None
|
|
), "One of speed or duration required"
|
|
self.widget = widget
|
|
self.styles = styles
|
|
self.start_time = start_time
|
|
self.attribute = attribute
|
|
self.final_value = value
|
|
self.easing = easing
|
|
self.on_complete = on_complete
|
|
self.level = level
|
|
|
|
size = widget.outer_size
|
|
viewport = widget.app.size
|
|
|
|
self.start = getattr(styles, attribute).resolve(size, viewport)
|
|
self.destination = value.resolve(size, viewport)
|
|
|
|
if speed is not None:
|
|
distance = self.start.get_distance_to(self.destination)
|
|
self.duration = distance / speed
|
|
else:
|
|
assert duration is not None, "Duration expected to be non-None"
|
|
self.duration = duration
|
|
|
|
def __call__(
|
|
self, time: float, app_animation_level: AnimationLevel = "full"
|
|
) -> bool:
|
|
factor = min(1.0, (time - self.start_time) / self.duration)
|
|
eased_factor = self.easing(factor)
|
|
|
|
if (
|
|
eased_factor >= 1
|
|
or app_animation_level == "none"
|
|
or app_animation_level == "basic"
|
|
and self.level == "full"
|
|
):
|
|
setattr(self.styles, self.attribute, self.final_value)
|
|
return True
|
|
|
|
if hasattr(self.start, "blend"):
|
|
value = self.start.blend(self.destination, eased_factor)
|
|
else:
|
|
value = self.start + (self.destination - self.start) * eased_factor
|
|
current = self.styles.get_rule(self.attribute)
|
|
if current != value:
|
|
setattr(self.styles, self.attribute, value)
|
|
|
|
return False
|
|
|
|
async def stop(self, complete: bool = True) -> None:
|
|
"""Stop the animation.
|
|
|
|
Args:
|
|
complete: Flag to say if the animation should be taken to completion.
|
|
|
|
Note:
|
|
[`on_complete`][Animation.on_complete] will be called regardless
|
|
of the value provided for `complete`.
|
|
"""
|
|
if complete:
|
|
setattr(self.styles, self.attribute, self.final_value)
|
|
await self.invoke_callback()
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, ScalarAnimation):
|
|
return (
|
|
self.final_value == other.final_value
|
|
and self.duration == other.duration
|
|
)
|
|
return False
|