mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
animation system
This commit is contained in:
@@ -12,7 +12,7 @@ App > View {
|
||||
layer: panels;
|
||||
border-right: outer #09312e;
|
||||
offset-x: -100%;
|
||||
transition: offset-x 1.2s in_cubic 200ms, offset-y 1s linear;
|
||||
transition: offset 1.2s in_cubic 200ms;
|
||||
}
|
||||
|
||||
#sidebar.-active {
|
||||
|
||||
@@ -8,14 +8,15 @@ from typing import Callable, TypeVar
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from . import log
|
||||
from ._easing import DEFAULT_EASING, EASING
|
||||
from ._timer import Timer
|
||||
from ._types import MessageTarget
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Protocol
|
||||
from typing import Protocol, runtime_checkable
|
||||
else:
|
||||
from typing_extensions import Protocol
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
|
||||
|
||||
EasingFunction = Callable[[float], float]
|
||||
@@ -23,6 +24,7 @@ EasingFunction = Callable[[float], float]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Animatable(Protocol):
|
||||
def blend(self: T, destination: T, factor: float) -> T:
|
||||
...
|
||||
@@ -47,28 +49,30 @@ class Animation:
|
||||
def blend(start: AnimatableT, end: AnimatableT, factor: float) -> AnimatableT:
|
||||
return start.blend(end, factor)
|
||||
|
||||
blend_function = (
|
||||
blend_float if isinstance(self.start_value, (int, float)) else blend
|
||||
)
|
||||
|
||||
if self.duration == 0:
|
||||
value = self.end_value
|
||||
else:
|
||||
factor = min(1.0, (time - self.start_time) / self.duration)
|
||||
eased_factor = self.easing_function(factor)
|
||||
# value = blend_function(self.start_value, self.end_value, eased_factor)
|
||||
|
||||
if self.end_value > self.start_value:
|
||||
eased_factor = self.easing_function(factor)
|
||||
value = (
|
||||
self.start_value
|
||||
+ (self.end_value - self.start_value) * eased_factor
|
||||
)
|
||||
if isinstance(self.start_value, Animatable):
|
||||
assert isinstance(self.end_value, Animatable)
|
||||
value = self.start_value.blend(self.end_value, eased_factor)
|
||||
else:
|
||||
eased_factor = 1 - self.easing_function(factor)
|
||||
value = (
|
||||
self.end_value + (self.start_value - self.end_value) * eased_factor
|
||||
)
|
||||
assert isinstance(self.start_value, float)
|
||||
assert isinstance(self.end_value, float)
|
||||
if self.end_value > self.start_value:
|
||||
eased_factor = self.easing_function(factor)
|
||||
value = (
|
||||
self.start_value
|
||||
+ (self.end_value - self.start_value) * eased_factor
|
||||
)
|
||||
else:
|
||||
eased_factor = 1 - self.easing_function(factor)
|
||||
value = (
|
||||
self.end_value
|
||||
+ (self.start_value - self.end_value) * eased_factor
|
||||
)
|
||||
setattr(self.obj, self.attribute, value)
|
||||
return value == self.end_value
|
||||
|
||||
@@ -134,7 +138,7 @@ class Animator:
|
||||
speed: float | None = None,
|
||||
easing: EasingFunction | str = DEFAULT_EASING,
|
||||
) -> None:
|
||||
|
||||
log("animate", obj, attribute, value)
|
||||
start_time = time()
|
||||
|
||||
animation_key = (obj, attribute)
|
||||
|
||||
@@ -10,6 +10,7 @@ from rich.color import Color
|
||||
import rich.repr
|
||||
from rich.style import Style
|
||||
|
||||
from .. import log
|
||||
from .errors import StyleValueError
|
||||
from ._error_tools import friendly_list
|
||||
from .constants import (
|
||||
@@ -134,6 +135,18 @@ class Styles:
|
||||
layers = NameListProperty()
|
||||
transitions = TransitionsProperty()
|
||||
|
||||
ANIMATABLE = {
|
||||
"offset-x",
|
||||
"offset-y",
|
||||
"offset",
|
||||
"padding",
|
||||
"margin",
|
||||
"width",
|
||||
"height",
|
||||
"min_width",
|
||||
"min_height",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@lru_cache(maxsize=1024)
|
||||
def parse(cls, css: str, path: str) -> Styles:
|
||||
@@ -173,6 +186,12 @@ class Styles:
|
||||
def has_offset(self) -> bool:
|
||||
return self._rule_offset is not None
|
||||
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
if key in self.ANIMATABLE:
|
||||
return self.transitions.get(key, None)
|
||||
else:
|
||||
return None
|
||||
|
||||
def extract_rules(
|
||||
self, specificity: tuple[int, int, int]
|
||||
) -> list[tuple[str, tuple[int, int, int, int], Any]]:
|
||||
@@ -188,7 +207,7 @@ class Styles:
|
||||
]
|
||||
return rules
|
||||
|
||||
def apply_rules(self, rules: Iterable[tuple[str, Any]]):
|
||||
def apply_rules(self, rules: Iterable[tuple[str, object]]):
|
||||
for key, value in rules:
|
||||
setattr(self, f"_rule_{key}", value)
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class Stylesheet:
|
||||
for name, specificity_rules in rule_attributes.items()
|
||||
]
|
||||
|
||||
node.styles.apply_rules(node_rules)
|
||||
node.apply_style_rules(node_rules)
|
||||
|
||||
def update(self, root: DOMNode) -> None:
|
||||
"""Update a node and its children."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast, Iterable, Iterator, TYPE_CHECKING
|
||||
from typing import Any, cast, Iterable, Iterator, TYPE_CHECKING
|
||||
|
||||
from rich.highlighter import ReprHighlighter
|
||||
import rich.repr
|
||||
@@ -135,11 +135,14 @@ class DOMNode(MessagePump):
|
||||
add_children(tree, self)
|
||||
return tree
|
||||
|
||||
def apply_style_rules(self, rules: Iterable[tuple[str, Any]]) -> None:
|
||||
self.styles.apply_rules(rules)
|
||||
|
||||
def reset_styles(self) -> None:
|
||||
from .widget import Widget
|
||||
|
||||
for node in self.walk_children():
|
||||
node.styles = Styles()
|
||||
# node.styles = Styles()
|
||||
if isinstance(node, Widget):
|
||||
node.clear_render_cache()
|
||||
|
||||
@@ -185,7 +188,6 @@ class DOMNode(MessagePump):
|
||||
"""Toggle class names"""
|
||||
self._classes.symmetric_difference_update(class_names)
|
||||
self.app.stylesheet.update(self.app)
|
||||
self.log(self.styles.css)
|
||||
|
||||
def has_psuedo_class(self, *class_names: str) -> bool:
|
||||
"""Check for psuedo class (such as hover, focus etc)"""
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
NamedTuple,
|
||||
cast,
|
||||
)
|
||||
@@ -236,6 +237,30 @@ class Widget(DOMNode):
|
||||
offset_x, offset_y = self.root_view.get_offset(self)
|
||||
return self.root_view.get_style_at(x + offset_x, y + offset_y)
|
||||
|
||||
def apply_style_rules(self, rules: Iterable[tuple[str, Any]]) -> None:
|
||||
styles = self.styles
|
||||
is_animatable = styles.ANIMATABLE.__contains__
|
||||
for key, value in rules:
|
||||
current = getattr(styles, f"_rule_{key}")
|
||||
if current == value:
|
||||
continue
|
||||
self.log(key, "=", value)
|
||||
if is_animatable(key):
|
||||
self.log("animatable", key)
|
||||
transition = styles.get_transition(key)
|
||||
self.log("transition", transition)
|
||||
if transition is None:
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
else:
|
||||
duration, easing, delay = transition
|
||||
self.log("ANIMATING")
|
||||
self.app.animator.animate(
|
||||
styles, key, value, duration=duration, easing=easing
|
||||
)
|
||||
else:
|
||||
self.log("not animatable")
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
|
||||
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
await self.app.call_later(callback, *args, **kwargs)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user