Skip to content

Commit e7399d4

Browse files
authored
Merge pull request #5575 from Textualize/user-interupt-scroll-end
Allow user to interupt scroll to end
2 parents 606fc1c + 59c9d25 commit e7399d4

File tree

5 files changed

+36
-11
lines changed

5 files changed

+36
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- Fixed IndexError in OptionList https://github.com/Textualize/textual/pull/5574
1414
- Fixed issue with clear_panes breaking tabbed content https://github.com/Textualize/textual/pull/5573
1515

16+
## Changed
17+
18+
- The user can now interrupt a scroll to end by grabbing the scrollbar or scrolling in any other way. Press ++end++ or scroll to the end to restore default behavior. This is more intuitive that it may sound.
19+
1620
## [2.1.0] - 2025-02-19
1721

1822
### Fixed

src/textual/scrollbar.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ async def _on_mouse_up(self, event: events.MouseUp) -> None:
356356
event.stop()
357357

358358
def _on_mouse_capture(self, event: events.MouseCapture) -> None:
359+
if isinstance(self._parent, Widget):
360+
self._parent._user_scroll_interrupt = True
359361
self.grabbed = event.mouse_position
360362
self.grabbed_position = self.position
361363

src/textual/widget.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ def __init__(
502502
"""Used to cache :odd pseudoclass state."""
503503
self._last_scroll_time = monotonic()
504504
"""Time of last scroll."""
505+
self._user_scroll_interrupt: bool = False
506+
"""Has the user interrupted a scroll to end?"""
505507

506508
@property
507509
def is_mounted(self) -> bool:
@@ -2424,6 +2426,9 @@ def _animate_on_complete() -> None:
24242426
if on_complete is not None:
24252427
self.call_next(on_complete)
24262428

2429+
if y is not None and maybe_scroll_y and y >= self.max_scroll_y:
2430+
self._user_scroll_interrupt = False
2431+
24272432
if animate:
24282433
# TODO: configure animation speed
24292434
if duration is None and speed is None:
@@ -2546,6 +2551,11 @@ def scroll_to(
25462551
Note:
25472552
The call to scroll is made after the next refresh.
25482553
"""
2554+
animator = self.app.animator
2555+
if x is not None:
2556+
animator.force_stop_animation(self, "scroll_x")
2557+
if y is not None:
2558+
animator.force_stop_animation(self, "scroll_y")
25492559
if immediate:
25502560
self._scroll_to(
25512561
x,
@@ -2688,9 +2698,20 @@ def scroll_end(
26882698
y_axis: Allow scrolling on Y axis?
26892699
26902700
"""
2701+
2702+
if self._user_scroll_interrupt and not force:
2703+
# Do not scroll to end if a user action has interrupted scrolling
2704+
return
2705+
26912706
if speed is None and duration is None:
26922707
duration = 1.0
26932708

2709+
async def scroll_end_on_complete() -> None:
2710+
"""It's possible new content was added before we reached the end."""
2711+
self.scroll_y = self.max_scroll_y
2712+
if on_complete is not None:
2713+
self.call_next(on_complete)
2714+
26942715
# In most cases we'd call self.scroll_to and let it handle the call
26952716
# to do things after a refresh, but here we need the refresh to
26962717
# happen first so that we can get the new self.max_scroll_y (that
@@ -2707,7 +2728,7 @@ def _lazily_scroll_end() -> None:
27072728
duration=duration,
27082729
easing=easing,
27092730
force=force,
2710-
on_complete=on_complete,
2731+
on_complete=scroll_end_on_complete,
27112732
level=level,
27122733
)
27132734

@@ -4450,13 +4471,15 @@ def _on_unmount(self) -> None:
44504471
def action_scroll_home(self) -> None:
44514472
if not self._allow_scroll:
44524473
raise SkipAction()
4474+
self._user_scroll_interrupt = True
44534475
self._clear_anchor()
44544476
self.scroll_home(x_axis=self.scroll_y == 0)
44554477

44564478
def action_scroll_end(self) -> None:
44574479
if not self._allow_scroll:
44584480
raise SkipAction()
44594481
self._clear_anchor()
4482+
self._user_scroll_interrupt = False
44604483
self.scroll_end(x_axis=self.scroll_y == self.is_vertical_scroll_end)
44614484

44624485
def action_scroll_left(self) -> None:
@@ -4474,24 +4497,28 @@ def action_scroll_right(self) -> None:
44744497
def action_scroll_up(self) -> None:
44754498
if not self.allow_vertical_scroll:
44764499
raise SkipAction()
4500+
self._user_scroll_interrupt = True
44774501
self._clear_anchor()
44784502
self.scroll_up()
44794503

44804504
def action_scroll_down(self) -> None:
44814505
if not self.allow_vertical_scroll:
44824506
raise SkipAction()
4507+
self._user_scroll_interrupt = True
44834508
self._clear_anchor()
44844509
self.scroll_down()
44854510

44864511
def action_page_down(self) -> None:
44874512
if not self.allow_vertical_scroll:
44884513
raise SkipAction()
4514+
self._user_scroll_interrupt = True
44894515
self._clear_anchor()
44904516
self.scroll_page_down()
44914517

44924518
def action_page_up(self) -> None:
44934519
if not self.allow_vertical_scroll:
44944520
raise SkipAction()
4521+
self._user_scroll_interrupt = True
44954522
self._clear_anchor()
44964523
self.scroll_page_up()
44974524

src/textual/widgets/_log.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,7 @@ def write(
191191
self._prune_max_lines()
192192

193193
auto_scroll = self.auto_scroll if scroll_end is None else scroll_end
194-
if (
195-
auto_scroll
196-
and not self.is_vertical_scrollbar_grabbed
197-
and is_vertical_scroll_end
198-
):
194+
if auto_scroll:
199195
self.scroll_end(animate=False, immediate=True, x_axis=False)
200196
return self
201197

src/textual/widgets/_rich_log.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,7 @@ def write(
279279
# the new line(s), and the height will definitely have changed.
280280
self.virtual_size = Size(self._widest_line_width, len(self.lines))
281281

282-
if (
283-
auto_scroll
284-
and not self.is_vertical_scrollbar_grabbed
285-
and is_vertical_scroll_end
286-
):
282+
if auto_scroll:
287283
self.scroll_end(animate=animate, immediate=False, x_axis=False)
288284

289285
return self

0 commit comments

Comments
 (0)