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:
Will McGugan
2023-04-24 09:33:15 +01:00
committed by GitHub
parent 8b6d9027e9
commit 80f4c12e76
9 changed files with 53 additions and 38 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed ### Changed
- `textual run` execs apps in a new context. - `textual run` execs apps in a new context.
- Textual console no longer parses console markup.
### Added ### Added

View File

@@ -211,9 +211,9 @@ class DevtoolsClient:
log: The log to write to devtools log: The log to write to devtools
""" """
if isinstance(log.objects_or_string, str): if isinstance(log.objects_or_string, str):
self.console.print(log.objects_or_string) self.console.print(log.objects_or_string, markup=False)
else: else:
self.console.print(*log.objects_or_string) self.console.print(*log.objects_or_string, markup=False)
segments = self.console.export_segments() segments = self.console.export_segments()

View File

@@ -50,15 +50,6 @@ class Callback(Event, bubble=False, verbose=True):
yield "callback", self.callback 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): class ShutdownRequest(Event):
pass pass

View File

@@ -374,6 +374,7 @@ class MessagePump(metaclass=_MessagePumpMeta):
**kwargs: Keyword arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable.
""" """
self._next_callbacks.append(partial(callback, *args, **kwargs)) self._next_callbacks.append(partial(callback, *args, **kwargs))
self.check_idle()
def _on_invoke_later(self, message: messages.InvokeLater) -> None: def _on_invoke_later(self, message: messages.InvokeLater) -> None:
# Forward InvokeLater message to the Screen # Forward InvokeLater message to the Screen
@@ -506,6 +507,7 @@ class MessagePump(metaclass=_MessagePumpMeta):
except Exception as error: except Exception as error:
self.app._handle_exception(error) self.app._handle_exception(error)
break break
await self._flush_next_callbacks()
async def _flush_next_callbacks(self) -> None: async def _flush_next_callbacks(self) -> None:
"""Invoke pending callbacks in next callbacks queue.""" """Invoke pending callbacks in next callbacks queue."""

View File

@@ -100,11 +100,11 @@ class Pilot(Generic[ReturnType]):
target_widget, offset, button=1, shift=shift, meta=meta, control=control target_widget, offset, button=1, shift=shift, meta=meta, control=control
) )
app.post_message(MouseDown(**message_arguments)) app.post_message(MouseDown(**message_arguments))
await self.pause() await self.pause(0.1)
app.post_message(MouseUp(**message_arguments)) app.post_message(MouseUp(**message_arguments))
await self.pause() await self.pause(0.1)
app.post_message(Click(**message_arguments)) app.post_message(Click(**message_arguments))
await self.pause() await self.pause(0.1)
async def hover( async def hover(
self, self,

View File

@@ -444,15 +444,12 @@ class Screen(Generic[ScreenResultType], Widget):
or self._dirty_widgets or self._dirty_widgets
): ):
self._update_timer.resume() self._update_timer.resume()
return
# The Screen is idle - a good opportunity to invoke the scheduled callbacks await self._invoke_and_clear_callbacks()
if self._callbacks:
self._on_timer_update()
def _on_timer_update(self) -> None: def _on_timer_update(self) -> None:
"""Called by the _update_timer.""" """Called by the _update_timer."""
self._update_timer.pause() self._update_timer.pause()
if self.is_current: if self.is_current:
if self._layout_required: if self._layout_required:
@@ -465,12 +462,11 @@ class Screen(Generic[ScreenResultType], Widget):
self._scroll_required = False self._scroll_required = False
if self._repaint_required: if self._repaint_required:
self._update_timer.resume()
self._dirty_widgets.clear() self._dirty_widgets.clear()
self._dirty_widgets.add(self) self._dirty_widgets.add(self)
self._repaint_required = False self._repaint_required = False
if self._dirty_widgets and self.is_current: if self._dirty_widgets:
self._compositor.update_widgets(self._dirty_widgets) self._compositor.update_widgets(self._dirty_widgets)
update = self._compositor.render_update( update = self._compositor.render_update(
screen_stack=self.app._background_screens screen_stack=self.app._background_screens
@@ -479,20 +475,12 @@ class Screen(Generic[ScreenResultType], Widget):
self._dirty_widgets.clear() self._dirty_widgets.clear()
if self._callbacks: if self._callbacks:
self.post_message(events.InvokeCallbacks()) self.call_next(self._invoke_and_clear_callbacks)
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()
async def _invoke_and_clear_callbacks(self) -> None: async def _invoke_and_clear_callbacks(self) -> None:
"""If there are scheduled callbacks to run, call them and clear """If there are scheduled callbacks to run, call them and clear
the callback queue.""" the callback queue."""
if self._callbacks: if self._callbacks:
display_update = self._compositor.render_update(
screen_stack=self.app._background_screens
)
self.app._display(self, display_update)
callbacks = self._callbacks[:] callbacks = self._callbacks[:]
self._callbacks.clear() self._callbacks.clear()
for callback in callbacks: for callback in callbacks:

View File

@@ -7,6 +7,7 @@ from __future__ import annotations
from rich.console import RenderableType from rich.console import RenderableType
from ._animator import EasingFunction
from .containers import ScrollableContainer from .containers import ScrollableContainer
from .geometry import Size from .geometry import Size
@@ -111,3 +112,37 @@ class ScrollView(ScrollableContainer):
from rich.panel import Panel from rich.panel import Panel
return Panel(f"{self.scroll_offset} {self.show_vertical_scrollbar}") 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,
)

View File

@@ -131,20 +131,18 @@ class Timer:
continue continue
now = _time.get_time() now = _time.get_time()
wait_time = max(0, next_timer - now) wait_time = max(0, next_timer - now)
if wait_time > 1 / 1000:
await sleep(wait_time) await sleep(wait_time)
count += 1 count += 1
try:
await self._tick(next_timer=next_timer, count=count)
except EventTargetGone:
break
await self._active.wait() await self._active.wait()
if self._reset: if self._reset:
start = _time.get_time() start = _time.get_time()
count = 0 count = 0
self._reset = False self._reset = False
continue continue
try:
await self._tick(next_timer=next_timer, count=count)
except EventTargetGone:
break
async def _tick(self, *, next_timer: float, count: int) -> None: 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""" """Triggers the Timer's action: either call its callback, or sends an event to its target"""

View File

@@ -39,4 +39,4 @@ async def test_call_after_refresh() -> None:
app.call_after_refresh(callback) app.call_after_refresh(callback)
await asyncio.wait_for(called_event.wait(), 1) await asyncio.wait_for(called_event.wait(), 1)
app_display_count = app.display_count app_display_count = app.display_count
assert app_display_count > display_count assert app_display_count == display_count