mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Fix scroll flicker (#2358)
* fix scroll flicker * fix scroll flicker * remove event * do not delay scroll * remove comment * test fix * remove commented code * comment * increase pause on click * changelog [skip ci] * wait on resume * remove note [skip ci]
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
### Changed
|
||||
|
||||
- `textual run` execs apps in a new context.
|
||||
- Textual console no longer parses console markup.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -211,9 +211,9 @@ class DevtoolsClient:
|
||||
log: The log to write to devtools
|
||||
"""
|
||||
if isinstance(log.objects_or_string, str):
|
||||
self.console.print(log.objects_or_string)
|
||||
self.console.print(log.objects_or_string, markup=False)
|
||||
else:
|
||||
self.console.print(*log.objects_or_string)
|
||||
self.console.print(*log.objects_or_string, markup=False)
|
||||
|
||||
segments = self.console.export_segments()
|
||||
|
||||
|
||||
@@ -50,15 +50,6 @@ class Callback(Event, bubble=False, verbose=True):
|
||||
yield "callback", self.callback
|
||||
|
||||
|
||||
class InvokeCallbacks(Event, bubble=False, verbose=True):
|
||||
"""An internal event, sent to the screen to run callbacks.
|
||||
|
||||
- [ ] Bubbles
|
||||
- [X] Verbose
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ShutdownRequest(Event):
|
||||
pass
|
||||
|
||||
|
||||
@@ -374,6 +374,7 @@ class MessagePump(metaclass=_MessagePumpMeta):
|
||||
**kwargs: Keyword arguments to pass to the callable.
|
||||
"""
|
||||
self._next_callbacks.append(partial(callback, *args, **kwargs))
|
||||
self.check_idle()
|
||||
|
||||
def _on_invoke_later(self, message: messages.InvokeLater) -> None:
|
||||
# Forward InvokeLater message to the Screen
|
||||
@@ -506,6 +507,7 @@ class MessagePump(metaclass=_MessagePumpMeta):
|
||||
except Exception as error:
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
await self._flush_next_callbacks()
|
||||
|
||||
async def _flush_next_callbacks(self) -> None:
|
||||
"""Invoke pending callbacks in next callbacks queue."""
|
||||
|
||||
@@ -100,11 +100,11 @@ class Pilot(Generic[ReturnType]):
|
||||
target_widget, offset, button=1, shift=shift, meta=meta, control=control
|
||||
)
|
||||
app.post_message(MouseDown(**message_arguments))
|
||||
await self.pause()
|
||||
await self.pause(0.1)
|
||||
app.post_message(MouseUp(**message_arguments))
|
||||
await self.pause()
|
||||
await self.pause(0.1)
|
||||
app.post_message(Click(**message_arguments))
|
||||
await self.pause()
|
||||
await self.pause(0.1)
|
||||
|
||||
async def hover(
|
||||
self,
|
||||
|
||||
@@ -444,15 +444,12 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
or self._dirty_widgets
|
||||
):
|
||||
self._update_timer.resume()
|
||||
return
|
||||
|
||||
# The Screen is idle - a good opportunity to invoke the scheduled callbacks
|
||||
|
||||
if self._callbacks:
|
||||
self._on_timer_update()
|
||||
await self._invoke_and_clear_callbacks()
|
||||
|
||||
def _on_timer_update(self) -> None:
|
||||
"""Called by the _update_timer."""
|
||||
|
||||
self._update_timer.pause()
|
||||
if self.is_current:
|
||||
if self._layout_required:
|
||||
@@ -465,12 +462,11 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
self._scroll_required = False
|
||||
|
||||
if self._repaint_required:
|
||||
self._update_timer.resume()
|
||||
self._dirty_widgets.clear()
|
||||
self._dirty_widgets.add(self)
|
||||
self._repaint_required = False
|
||||
|
||||
if self._dirty_widgets and self.is_current:
|
||||
if self._dirty_widgets:
|
||||
self._compositor.update_widgets(self._dirty_widgets)
|
||||
update = self._compositor.render_update(
|
||||
screen_stack=self.app._background_screens
|
||||
@@ -479,20 +475,12 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
self._dirty_widgets.clear()
|
||||
|
||||
if self._callbacks:
|
||||
self.post_message(events.InvokeCallbacks())
|
||||
|
||||
async def _on_invoke_callbacks(self, event: events.InvokeCallbacks) -> None:
|
||||
"""Handle PostScreenUpdate events, which are sent after the screen is updated"""
|
||||
await self._invoke_and_clear_callbacks()
|
||||
self.call_next(self._invoke_and_clear_callbacks)
|
||||
|
||||
async def _invoke_and_clear_callbacks(self) -> None:
|
||||
"""If there are scheduled callbacks to run, call them and clear
|
||||
the callback queue."""
|
||||
if self._callbacks:
|
||||
display_update = self._compositor.render_update(
|
||||
screen_stack=self.app._background_screens
|
||||
)
|
||||
self.app._display(self, display_update)
|
||||
callbacks = self._callbacks[:]
|
||||
self._callbacks.clear()
|
||||
for callback in callbacks:
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
from rich.console import RenderableType
|
||||
|
||||
from ._animator import EasingFunction
|
||||
from .containers import ScrollableContainer
|
||||
from .geometry import Size
|
||||
|
||||
@@ -111,3 +112,37 @@ class ScrollView(ScrollableContainer):
|
||||
from rich.panel import Panel
|
||||
|
||||
return Panel(f"{self.scroll_offset} {self.show_vertical_scrollbar}")
|
||||
|
||||
# Custom scroll to which doesn't require call_after_refresh
|
||||
def scroll_to(
|
||||
self,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
*,
|
||||
animate: bool = True,
|
||||
speed: float | None = None,
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Scroll to a given (absolute) coordinate, optionally animating.
|
||||
|
||||
Args:
|
||||
x: X coordinate (column) to scroll to, or `None` for no change.
|
||||
y: Y coordinate (row) to scroll to, or `None` for no change.
|
||||
animate: Animate to new scroll position.
|
||||
speed: Speed of scroll if `animate` is `True`; or `None` to use `duration`.
|
||||
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
|
||||
easing: An easing method for the scrolling animation.
|
||||
force: Force scrolling even when prohibited by overflow styling.
|
||||
"""
|
||||
|
||||
self._scroll_to(
|
||||
x,
|
||||
y,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
@@ -131,20 +131,18 @@ class Timer:
|
||||
continue
|
||||
now = _time.get_time()
|
||||
wait_time = max(0, next_timer - now)
|
||||
if wait_time > 1 / 1000:
|
||||
await sleep(wait_time)
|
||||
|
||||
await sleep(wait_time)
|
||||
count += 1
|
||||
try:
|
||||
await self._tick(next_timer=next_timer, count=count)
|
||||
except EventTargetGone:
|
||||
break
|
||||
await self._active.wait()
|
||||
if self._reset:
|
||||
start = _time.get_time()
|
||||
count = 0
|
||||
self._reset = False
|
||||
continue
|
||||
try:
|
||||
await self._tick(next_timer=next_timer, count=count)
|
||||
except EventTargetGone:
|
||||
break
|
||||
|
||||
async def _tick(self, *, next_timer: float, count: int) -> None:
|
||||
"""Triggers the Timer's action: either call its callback, or sends an event to its target"""
|
||||
|
||||
@@ -39,4 +39,4 @@ async def test_call_after_refresh() -> None:
|
||||
app.call_after_refresh(callback)
|
||||
await asyncio.wait_for(called_event.wait(), 1)
|
||||
app_display_count = app.display_count
|
||||
assert app_display_count > display_count
|
||||
assert app_display_count == display_count
|
||||
|
||||
Reference in New Issue
Block a user