Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ docs/build
docs/source/_build
tools/*.txt
playground/
.mypy_cache/
.screenshot_cache/

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down Expand Up @@ -129,3 +131,6 @@ sandbox/

# Used by mkdocs-material social plugin
.cache

# Snapshot tests output
__snapshots__/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [8.1.1] - 2026-03-10

### Fixed

- Hotfix for animation on complete https://github.com/Textualize/textual/pull/6412

## [8.1.0] - 2026-03-10

### Changed
Expand Down Expand Up @@ -3381,6 +3387,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

[8.1.1]: https://github.com/Textualize/textual/compare/v8.1.0...v8.1.1
[8.1.0]: https://github.com/Textualize/textual/compare/v8.0.2...v8.1.0
[8.0.2]: https://github.com/Textualize/textual/compare/v8.0.1...v8.0.2
[8.0.1]: https://github.com/Textualize/textual/compare/v8.0.0...v8.0.1
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "8.1.0"
version = "8.1.1"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down Expand Up @@ -36,6 +36,7 @@ include = [
# that works around that.
{ path = "docs-offline/**/*", format = "sdist" },
]
exclude = ["tests/snapshot_tests/__snapshots__"]

[tool.poetry.urls]
"Bug Tracker" = "https://github.com/Textualize/textual/issues"
Expand Down
5 changes: 1 addition & 4 deletions src/textual/_animator.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,10 +458,7 @@ def _animate(

if (current_animation := self._animations.get(animation_key)) is not None:
if (on_complete := current_animation.on_complete) is not None:
on_complete()
self._animations.pop(animation_key)
if current_animation == animation:
return
self.app.call_later(on_complete)

self._animations[animation_key] = animation
self._timer.resume()
Expand Down
81 changes: 68 additions & 13 deletions tests/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,34 +213,89 @@ async def test_cancel_widget_non_animation() -> None:
assert not pilot.app.animator.is_being_animated(widget, "counter")


async def test_double_animation_on_complete() -> None:
"""Test that animating an attribute a second time, fires its `on_complete` callback."""
async def test_double_animation_same_value_on_complete() -> None:
"""Test that animating an attribute a second time to the same value, fires its `on_complete` callback."""

complete_count = 0
completed: list[str] = []

class AnimApp(App):
x = var(0)

def on_key(self) -> None:
async def animate_a(self) -> None:

def on_complete() -> None:
nonlocal complete_count
complete_count += 1
completed.append("a")

self.animator.animate(
self,
"x",
100 + complete_count,
100,
duration=0.1,
on_complete=on_complete,
)

async def animate_b(self) -> None:

def on_complete() -> None:
completed.append("b")

self.animator.animate(
self,
"x",
100,
duration=0.1,
on_complete=on_complete,
)

app = AnimApp()
async with app.run_test() as pilot:
await app.animate_a()
assert app.x != 100
await app.animate_b()
await pilot.wait_for_animation()
assert completed == ["a", "b"]
assert app.x == 100


async def test_double_animation_different_value_on_complete() -> None:
"""Test that animating an attribute a second time to a different value, fires its `on_complete` callback."""

completed: list[str] = []

class AnimApp(App):
x = var(0)

async def animate_a(self) -> None:

def on_complete() -> None:
completed.append("a")

self.animator.animate(
self,
"x",
100,
duration=0.1,
on_complete=on_complete,
)

async def animate_b(self) -> None:

def on_complete() -> None:
completed.append("b")

self.animator.animate(
self,
"x",
101,
duration=0.1,
on_complete=on_complete,
)

app = AnimApp()
async with app.run_test() as pilot:
# Press space twice to initiate 2 animations
await pilot.press("space")
await pilot.press("space")
# Wait for animations to complete
await app.animate_a()
assert app.x != 101
await app.animate_b() # animate to different value
await pilot.wait_for_animation()
# Check that on_complete callback was invoked twice
assert complete_count == 2
assert completed == ["a", "b"]
assert app.x == 101
Loading