Skip to content

Commit

Permalink
Merge pull request #5164 from Textualize/list-view-fix
Browse files Browse the repository at this point in the history
Fix list view flicker
  • Loading branch information
willmcgugan authored Oct 24, 2024
2 parents b82f5bf + b75269f commit 667b3b3
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 81 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `Containers.HorizontalGroup` and `Containers.VerticalGroup` https://github.com/Textualize/textual/pull/5113
- Added `$`, `£`, ``, `(`, `)` symbols to Digits https://github.com/Textualize/textual/pull/5113
- Added `Button.action` parameter to invoke action when clicked https://github.com/Textualize/textual/pull/5113
- Added `immediate` parameter to scroll methods https://github.com/Textualize/textual/pull/5164
- Added `textual._loop.loop_from_index` https://github.com/Textualize/textual/pull/5164

### Fixed

- Fixed glitchy ListView https://github.com/Textualize/textual/issues/5163

## [0.84.0] - 2024-10-22

Expand Down
43 changes: 42 additions & 1 deletion src/textual/_loop.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Iterable, TypeVar
from typing import Iterable, Literal, Sequence, TypeVar

T = TypeVar("T")

Expand Down Expand Up @@ -43,3 +43,44 @@ def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
first = False
previous_value = value
yield first, True, previous_value


def loop_from_index(
values: Sequence[T],
index: int,
direction: Literal[-1, +1] = +1,
wrap: bool = True,
) -> Iterable[tuple[int, T]]:
"""Iterate over values in a sequence from a given starting index, potentially wrapping the index
if it would go out of bounds.
Note that the first value to be yielded is a step from `index`, and `index` will be yielded *last*.
Args:
values: A sequence of values.
index: Starting index.
direction: Direction to move index (+1 for forward, -1 for backward).
bool: Should the index wrap when out of bounds?
Yields:
A tuple of index and value from the sequence.
"""
# Sanity check for devs who miss the typing errors
assert direction in (-1, +1), "direction must be -1 or +1"
count = len(values)
if wrap:
for _ in range(count):
index = (index + direction) % count
yield (index, values[index])
else:
if direction == +1:
for _ in range(count):
if (index := index + 1) >= count:
break
yield (index, values[index])
else:
for _ in range(count):
if (index := index - 1) < 0:
break
yield (index, values[index])
2 changes: 1 addition & 1 deletion src/textual/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ def timer(subject: str = "time") -> Generator[None, None, None]:
yield
elapsed = perf_counter() - start
elapsed_ms = elapsed * 1000
log(f"{subject} elapsed {elapsed_ms:.2f}ms")
log(f"{subject} elapsed {elapsed_ms:.4f}ms")
21 changes: 6 additions & 15 deletions src/textual/_widget_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

from __future__ import annotations

from functools import partial
from itertools import count
from typing import Literal, Protocol, Sequence

from typing_extensions import TypeAlias

from textual._loop import loop_from_index


class Disableable(Protocol):
"""Non-widgets that have an enabled/disabled status."""
Expand Down Expand Up @@ -105,7 +106,6 @@ def find_next_enabled(
candidates: Sequence[Disableable],
anchor: int | None,
direction: Direction,
with_anchor: bool = False,
) -> int | None:
"""Find the next enabled object if we're currently at the given anchor.
Expand All @@ -118,8 +118,6 @@ def find_next_enabled(
enabled object.
direction: The direction in which to traverse the candidates when looking for
the next enabled candidate.
with_anchor: Consider the anchor position as the first valid position instead of
the last one.
Returns:
The next enabled object. If none are available, return the anchor.
Expand All @@ -134,17 +132,10 @@ def find_next_enabled(
)
return None

start = anchor + direction if not with_anchor else anchor
key_function = partial(
get_directed_distance,
start=start,
direction=direction,
wrap_at=len(candidates),
)
enabled_candidates = [
index for index, candidate in enumerate(candidates) if not candidate.disabled
]
return min(enabled_candidates, key=key_function, default=anchor)
for index, candidate in loop_from_index(candidates, anchor, direction, wrap=True):
if not candidate.disabled:
return index
return anchor


def find_next_enabled_no_wrap(
Expand Down
3 changes: 3 additions & 0 deletions src/textual/scroll_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def scroll_to(
force: bool = False,
on_complete: CallbackType | None = None,
level: AnimationLevel = "basic",
immediate: bool = False,
) -> None:
"""Scroll to a given (absolute) coordinate, optionally animating.
Expand All @@ -136,6 +137,8 @@ def scroll_to(
force: Force scrolling even when prohibited by overflow styling.
on_complete: A callable to invoke when the animation is finished.
level: Minimum level required for the animation to take place (inclusive).
immediate: If `False` the scroll will be deferred until after a screen refresh,
set to `True` to scroll immediately.
"""

self._scroll_to(
Expand Down
Loading

0 comments on commit 667b3b3

Please sign in to comment.