diff --git a/sandbox/darren/just_a_box.py b/sandbox/darren/just_a_box.py index 781c66f67..b9d0d44ca 100644 --- a/sandbox/darren/just_a_box.py +++ b/sandbox/darren/just_a_box.py @@ -1,11 +1,9 @@ from __future__ import annotations from rich.console import RenderableType -from rich.panel import Panel from textual import events from textual.app import App, ComposeResult -from textual.layout import Horizontal, Vertical from textual.widget import Widget @@ -18,37 +16,22 @@ class Box(Widget, can_focus=True): super().__init__(*children, id=id, classes=classes) def render(self) -> RenderableType: - return Panel("Box") + return "Box" class JustABox(App): def compose(self) -> ComposeResult: - yield Horizontal( - Vertical( - Box(id="box1", classes="box"), - Box(id="box2", classes="box"), - # Box(id="box3", classes="box"), - # Box(id="box4", classes="box"), - # Box(id="box5", classes="box"), - # Box(id="box6", classes="box"), - # Box(id="box7", classes="box"), - # Box(id="box8", classes="box"), - # Box(id="box9", classes="box"), - # Box(id="box10", classes="box"), - id="left_pane", - ), - Box(id="middle_pane"), - Vertical( - Box(id="boxa", classes="box"), - Box(id="boxb", classes="box"), - Box(id="boxc", classes="box"), - id="right_pane", - ), - id="horizontal", - ) + self.box = Box() + yield self.box - def key_p(self): - print(self.query("#horizontal").first().styles.layout) + def key_a(self): + self.animator.animate( + self.box.styles, + "opacity", + value=0.0, + duration=2.0, + on_complete=self.box.remove, + ) async def on_key(self, event: events.Key) -> None: await self.dispatch_key(event) diff --git a/src/textual/_animator.py b/src/textual/_animator.py index dbc5d9670..4260ef758 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -30,8 +30,20 @@ class Animatable(Protocol): class Animation(ABC): + + on_complete: Callable[[], None] | None = None + """Callback to run after animation completes""" + @abstractmethod def __call__(self, time: float) -> bool: # pragma: no cover + """Call the animation, return a boolean indicating whether animation is in-progress or complete. + + Args: + time (float): The current timestamp + + Returns: + bool: True if the animation has finished, otherwise False. + """ raise NotImplementedError("") def __eq__(self, other: object) -> bool: @@ -48,6 +60,7 @@ class SimpleAnimation(Animation): end_value: float | Animatable final_value: object easing: EasingFunction + on_complete: Callable[[], None] | None = None def __call__(self, time: float) -> bool: @@ -109,6 +122,7 @@ class BoundAnimator: duration: float | None = None, speed: float | None = None, easing: EasingFunction | str = DEFAULT_EASING, + on_complete: Callable[[], None] | None = None, ) -> None: easing_function = EASING[easing] if isinstance(easing, str) else easing return self._animator.animate( @@ -119,6 +133,7 @@ class BoundAnimator: duration=duration, speed=speed, easing=easing_function, + on_complete=on_complete, ) @@ -163,6 +178,7 @@ class Animator: duration: float | None = None, speed: float | None = None, easing: EasingFunction | str = DEFAULT_EASING, + on_complete: Callable[[], None] | None = None, ) -> None: """Animate an attribute to a new value. @@ -201,6 +217,7 @@ class Animator: duration=duration, speed=speed, easing=easing_function, + on_complete=on_complete, ) if animation is None: start_value = getattr(obj, attribute) @@ -223,6 +240,7 @@ class Animator: end_value=value, final_value=final_value, easing=easing_function, + on_complete=on_complete, ) assert animation is not None, "animation expected to be non-None" @@ -241,7 +259,11 @@ class Animator: animation_keys = list(self._animations.keys()) for animation_key in animation_keys: animation = self._animations[animation_key] - if animation(animation_time): + animation_complete = animation(animation_time) + if animation_complete: + completion_callback = animation.on_complete + if completion_callback is not None: + completion_callback() del self._animations[animation_key] def _get_time(self) -> float: diff --git a/src/textual/css/scalar_animation.py b/src/textual/css/scalar_animation.py index ae37249f9..e3b9c4d69 100644 --- a/src/textual/css/scalar_animation.py +++ b/src/textual/css/scalar_animation.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable -from .. import events, log from ..geometry import Offset from .._animator import Animation from .scalar import ScalarOffset @@ -25,6 +24,7 @@ class ScalarAnimation(Animation): duration: float | None, speed: float | None, easing: EasingFunction, + on_complete: Callable[[], None] | None = None, ): assert ( speed is not None or duration is not None @@ -35,6 +35,7 @@ class ScalarAnimation(Animation): self.attribute = attribute self.final_value = value self.easing = easing + self.on_complete = on_complete size = widget.outer_size viewport = widget.app.size @@ -55,7 +56,6 @@ class ScalarAnimation(Animation): eased_factor = self.easing(factor) if eased_factor >= 1: - offset = self.final_value setattr(self.styles, self.attribute, self.final_value) return True