Skip to content

Commit 5b27dfa

Browse files
committed
Allow user to interupt scroll to end
1 parent 49ff0e0 commit 5b27dfa

File tree

5 files changed

+28
-11
lines changed

5 files changed

+28
-11
lines changed

src/textual/scroll_view.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def watch_scroll_x(self, old_value: float, new_value: float) -> None:
3838
self.refresh()
3939

4040
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
41+
if new_value >= self.max_scroll_y:
42+
self._user_scroll_interrupt = False
4143
if self.show_vertical_scrollbar and (old_value) != (new_value):
4244
self.vertical_scrollbar.position = new_value
4345
self.refresh()

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: 22 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:
@@ -1698,6 +1700,8 @@ def watch_scroll_x(self, old_value: float, new_value: float) -> None:
16981700
self._refresh_scroll()
16991701

17001702
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
1703+
if new_value >= self.max_scroll_y:
1704+
self._user_scroll_interrupt = True
17011705
self.vertical_scrollbar.position = new_value
17021706
if round(old_value) != round(new_value):
17031707
self._refresh_scroll()
@@ -2688,9 +2692,20 @@ def scroll_end(
26882692
y_axis: Allow scrolling on Y axis?
26892693
26902694
"""
2695+
2696+
if self._user_scroll_interrupt and not force:
2697+
# Do not scroll to end if a user action has interrupted scrolling
2698+
return
2699+
26912700
if speed is None and duration is None:
26922701
duration = 1.0
26932702

2703+
async def scroll_end_on_complete() -> None:
2704+
"""It's possible new content was added before we reached the end."""
2705+
self.scroll_y = self.max_scroll_y
2706+
if on_complete is not None:
2707+
self.call_next(on_complete)
2708+
26942709
# In most cases we'd call self.scroll_to and let it handle the call
26952710
# to do things after a refresh, but here we need the refresh to
26962711
# happen first so that we can get the new self.max_scroll_y (that
@@ -2707,7 +2722,7 @@ def _lazily_scroll_end() -> None:
27072722
duration=duration,
27082723
easing=easing,
27092724
force=force,
2710-
on_complete=on_complete,
2725+
on_complete=scroll_end_on_complete,
27112726
level=level,
27122727
)
27132728

@@ -4450,13 +4465,15 @@ def _on_unmount(self) -> None:
44504465
def action_scroll_home(self) -> None:
44514466
if not self._allow_scroll:
44524467
raise SkipAction()
4468+
self._user_scroll_interrupt = True
44534469
self._clear_anchor()
44544470
self.scroll_home(x_axis=self.scroll_y == 0)
44554471

44564472
def action_scroll_end(self) -> None:
44574473
if not self._allow_scroll:
44584474
raise SkipAction()
44594475
self._clear_anchor()
4476+
self._user_scroll_interrupt = False
44604477
self.scroll_end(x_axis=self.scroll_y == self.is_vertical_scroll_end)
44614478

44624479
def action_scroll_left(self) -> None:
@@ -4474,24 +4491,28 @@ def action_scroll_right(self) -> None:
44744491
def action_scroll_up(self) -> None:
44754492
if not self.allow_vertical_scroll:
44764493
raise SkipAction()
4494+
self._user_scroll_interrupt = True
44774495
self._clear_anchor()
44784496
self.scroll_up()
44794497

44804498
def action_scroll_down(self) -> None:
44814499
if not self.allow_vertical_scroll:
44824500
raise SkipAction()
4501+
self._user_scroll_interrupt = True
44834502
self._clear_anchor()
44844503
self.scroll_down()
44854504

44864505
def action_page_down(self) -> None:
44874506
if not self.allow_vertical_scroll:
44884507
raise SkipAction()
4508+
self._user_scroll_interrupt = True
44894509
self._clear_anchor()
44904510
self.scroll_page_down()
44914511

44924512
def action_page_up(self) -> None:
44934513
if not self.allow_vertical_scroll:
44944514
raise SkipAction()
4515+
self._user_scroll_interrupt = True
44954516
self._clear_anchor()
44964517
self.scroll_page_up()
44974518

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)