Skip to content

Scroll bar: pause trough hold paging when the thumb reaches the cursor - #12895

Merged
niksedk merged 2 commits into
SubtitleEdit:mainfrom
ivandrofly:fix/12894-trough-hold-paging
Jul 28, 2026
Merged

Scroll bar: pause trough hold paging when the thumb reaches the cursor#12895
niksedk merged 2 commits into
SubtitleEdit:mainfrom
ivandrofly:fix/12894-trough-hold-paging

Conversation

@ivandrofly

@ivandrofly ivandrofly commented Jul 28, 2026

Copy link
Copy Markdown
Member

Fixes #12894

Summary

  • Click-and-hold on a DataGrid vertical scroll bar trough paged straight past the cursor to the end of the list. The theme's trough RepeatButton keeps repeating while IsPressed, and Button only re-evaluates IsPressed on PointerMoved, so with a stationary cursor the repeat never stopped. Invisible before SE 5.1.0 beta 4: Scroll bar anomalies #12051 (LargeChange was 1 px per tick); obvious now that a trough click pages a full viewport.
  • DataGridScrollBarBehavior now handles the plain (non-shift) trough press itself: pages once immediately, repeats on a DispatcherTimer (300 ms delay, then 100 ms — Avalonia's RepeatButton defaults), and pauses when the thumb reaches the pointer, resuming only if the pointer moves further along, matching the Windows scroll bar. The paging direction is latched at press time so an overshoot cannot ping-pong around the cursor.
  • Paging stops on pointer release (also outside the window, via pointer capture) and on capture loss (window deactivation, re-template mid-hold).
  • Extracted the existing trough hit-testing into a shared TryGetTroughPress helper used by both the new hold paging and the shipped shift+click jump, so thumb drags, line arrows, and shift+click are unchanged.
  • Added headless tests: a trough press pages exactly one viewport, release stops paging, and a press on the thumb does not page.

Test plan

  • dotnet test tests/UI/UITests.csproj — all tests pass (3 new in DataGridScrollBarTroughPagingTests)
  • Load a subtitle long enough to scroll; click-hold the trough below the thumb without moving the mouse: paging pauses when the thumb reaches the cursor instead of running to the end
  • Still holding, move the pointer further down: paging resumes; move it above the thumb: it stays paused (no reverse)
  • Single trough click scrolls exactly one page; Shift+trough click still jumps to position; thumb drag, arrow buttons, and mouse wheel unchanged
  • Same behavior in another grid (Options → Shortcuts), since the style is app-wide
  • Release the mouse outside the window mid-hold and Alt+Tab mid-hold: paging stops both times

🤖 Generated with Claude Code

SubtitleEdit#12894)

The theme's trough RepeatButton keeps repeating while IsPressed, and Button
only re-evaluates IsPressed on PointerMoved, so a stationary click-and-hold
paged straight past the cursor to the end. Handle the trough press in
DataGridScrollBarBehavior instead: page once, repeat on a timer, and pause
when the thumb reaches the pointer, resuming only if it moves further along,
like the Windows scroll bar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the DataGrid vertical scrollbar trough “runaway paging” behavior by moving press-and-hold paging logic out of the theme RepeatButton and into DataGridScrollBarBehavior, aligning behavior with Windows (pause when the thumb reaches the stationary pointer; resume only if the pointer moves further in the latched direction).

Changes:

  • Added custom press-and-hold trough paging using a DispatcherTimer, pointer capture, and latched paging direction.
  • Refactored trough hit-testing into a shared TryGetTroughPress helper used by both hold-paging and Shift+click jump.
  • Added headless UI tests covering single-page paging, stop-on-release, and “thumb press does not page”.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/ui/Logic/DataGridScrollBarBehavior.cs Implements custom trough hold-paging (timer + capture + pause-at-pointer) and shares trough hit-testing between behaviors.
tests/UI/Logic/DataGridScrollBarTroughPagingTests.cs Adds headless regression tests for basic trough paging and release/thumb-press behavior.

Comment on lines +186 to +190
private static void TickTroughHoldPaging(DataGrid grid, ScrollBar verticalScrollBar, TroughHoldState holdState)
{
var track = verticalScrollBar.GetVisualDescendants().OfType<Track>().FirstOrDefault();
if (track == null || track.Bounds.Height <= 0)
var thumb = track?.Thumb;
if (track == null || thumb == null)
Comment on lines +201 to 209
// Pause (not stop) once the thumb has reached the pointer: paging resumes if the
// pointer moves further in the latched direction, like the Windows scroll bar.
var shouldPage = holdState.PageDown
? posY > thumb.Bounds.Y + thumb.Bounds.Height
: posY < thumb.Bounds.Y;
if (shouldPage)
{
return;
PageOnce(grid, verticalScrollBar, holdState.PageDown);
}
…sume

Review follow-up on SubtitleEdit#12895:

- Cache the Track at press time instead of walking the visual tree on every
  100 ms tick (Copilot review).
- Stop the repeat if the pointer capture is gone, so a missed release cannot
  leave the timer paging.
- Skip the page (and the ProcessVerticalScroll call) once the value is clamped
  at the end of the range.
- Do not swallow the press when ProcessVerticalScroll is missing: without it a
  page would move the thumb but not the rows, so leave it to the theme.
- Keep the hold state on the scroll bar instead of in a closure.
- Tests: the headless dispatcher never fires a DispatcherTimer, so the repeat is
  stepped by hand - covers pause at the pointer, resume when it moves further,
  no reverse when it moves back, and release stopping the repeat (Copilot
  review). Verified by mutation: each test fails when its behavior is removed.
- Trim comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@niksedk

niksedk commented Jul 28, 2026

Copy link
Copy Markdown
Member

Pushed a follow-up commit addressing both review comments:

  • Track lookup per tick — the Track is now captured at press time and kept in the hold state, so the timer no longer walks the visual tree every 100 ms.
  • Missing pause/resume coverage — the headless dispatcher never fires a DispatcherTimer, so the repeat is now stepped by hand through an internal test hook. Added: pauses when the thumb reaches the pointer (and does not run to the end), resumes when the pointer moves further down, does not reverse when it moves back, and release stops the repeat. Each new test was verified by mutation — it fails when the behavior it covers is removed.

Also while in there:

  • The repeat stops if the pointer capture is gone, so a missed release cannot leave the timer paging.
  • No page (and no ProcessVerticalScroll call) once the value is clamped at the end of the range.
  • The press is no longer swallowed when ProcessVerticalScroll cannot be resolved — without it a page would move the thumb but not the rows, so it is better to leave that press to the theme.
  • Hold state lives on the scroll bar instead of a closure, and the comments are trimmed.

dotnet test tests/UI/UITests.csproj: 913 passed, 1 skipped.

@niksedk
niksedk merged commit b23b15a into SubtitleEdit:main Jul 28, 2026
1 check was pending
@niksedk niksedk mentioned this pull request Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scroll bar trough click-and-hold keeps paging past the cursor in the subtitle grid

3 participants