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
|
### 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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user