Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix list view flicker #5164

Merged
merged 13 commits into from
Oct 24, 2024
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ 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/).

## Unreleased

### Added

- 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

### Fixed
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
Loading