-
Often, you have a scroll view that might extend in height over time that you want to always be scrolled to the bottom unless the user scrolls up. This is common in a Chat UI with a vertical list of messages and message containers getting appended to the bottom. This is commonly done in CSS using Unless I missed something, this doesn't seem to be very easy to implement elegantly in Textual, and this does seem like it should be a first-party feature. I had quickly drafted a workaround: class VerticalScrollPinToBottom(VerticalScroll):
_layout_lock = asyncio.Lock()
_layout_lock_holder = None
async def _acquire_layout_lock(self) -> float:
await self._layout_lock.acquire()
lock_holder = time.time()
self._layout_lock_holder = lock_holder
return lock_holder
def _try_release_layout_lock(self, lock_holder: float) -> None:
if self._layout_lock.locked and self._layout_lock_holder == lock_holder:
self._layout_lock_holder = None
self._layout_lock.release()
async def _try_release_layout_lock_after(
self, lock_holder: float, delay: float
) -> None:
await asyncio.sleep(delay)
self._try_release_layout_lock(lock_holder)
async def update_with_scrolling(self, callback: Callable) -> None:
lock_holder = await self._acquire_layout_lock()
# Run the callback that increases the content height
if inspect.iscoroutinefunction(callback):
await callback()
else:
callback()
if self.max_scroll_y - self.scroll_target_y <= 20:
# Scroll to the end and release the lock after scrolling is done
self.scroll_end(
animate=False,
on_complete=functools.partial(
self._try_release_layout_lock, lock_holder
),
)
# Just in case, always release the lock after a delay
asyncio.create_task(
self._try_release_layout_lock_after(lock_holder, delay=0.1)
)
else:
# If we didn't scroll, release the lock immediately
self._try_release_layout_lock(lock_holder) However, it is visibly laggy with a slowed framerate at times and obvious delayed scrolling. This visual lag could be circumvented with the reversing approach in the CSS linked above. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
We found the following entry in the FAQ which you may find helpful: Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review. This is an automated reply, generated by FAQtory |
Beta Was this translation helpful? Give feedback.
-
Here's how this is currently implemented in the textual/src/textual/widgets/_log.py Lines 236 to 241 in af58978 |
Beta Was this translation helpful? Give feedback.
Without the lock, the scrolling was not able to catch up with the content being added.
I figured out the problem: it was the markdown parser. I update the markdown content as I am streaming tokens quite quickly in the message, and I suspect what's happening is the markdown parser is too slow and ends up blocking the event loop, which prevents the updates from being made in time.
Replaced it with a
Static
widget and it works fine now.