Skip to content

Diagonal (two-axis) precision-touchpad panning — step 4 of #33 (stacked on #34, #35)#36

Draft
andrewtheart wants to merge 3 commits into
FrozenAssassine:masterfrom
andrewtheart:feat/diagonal-touchpad-scroll
Draft

Diagonal (two-axis) precision-touchpad panning — step 4 of #33 (stacked on #34, #35)#36
andrewtheart wants to merge 3 commits into
FrozenAssassine:masterfrom
andrewtheart:feat/diagonal-touchpad-scroll

Conversation

@andrewtheart

Copy link
Copy Markdown
Contributor

Stacked on #34 and #35. This branch builds on the pixel-offset scroll seam (#34) and horizontal
virtualization (#35), so until those merge this PR's diff also includes their files. Please review #34#35
first; I'll rebase this onto master as they land. Opened as a draft for that reason.

Step 4 of the editor scroll/render series (#33)

The headline feature: diagonal (two-axis) precision-touchpad panning.

The problem

The editor scrolls via two ScrollBar primitives fed by the mouse wheel, which is single-axis: you can scroll
vertically or (with Shift) horizontally, but never both at once. On a precision touchpad, a diagonal
two-finger swipe should pan the document freely in both axes at the same time — as it does in every modern
scrolling surface. The wheel message stream physically can't express that 2-D delta.

The approach

A composition InteractionTracker + VisualInteractionSource on the selection canvas reads the raw 2-D
precision-touchpad manipulation delta and drives the pixel IScrollOffsetSource (#34). The two ScrollBar
primitives stay in sync automatically because the tracker writes through the same offset source they back.

The source is VisualInteractionSourceRedirectionMode.CapableTouchpadOnly, so it captures only the
precision-touchpad pan:

  • Mouse wheel, Shift+wheel, and Ctrl+wheel zoom keep flowing through the existing
    PointerActionsManager.PointerWheelAction untouched.
  • Mouse click / drag-selection is unaffected.
  • Precision-touchpad is auto-redirected by ManipulationRedirectionMode — no TryRedirectForManipulation
    call is needed (that's only for touch/pen).

A 50 ms timer keeps the tracker's MaxPosition sized to the content extent, and — critically — the tracker
follows programmatic scrolls immediately: it subscribes to the offset source's ViewChanged, so a
touchpad pan started right after a caret-follow / page-key / go-to-line / find-reveal / wheel scroll begins
from the current position instead of snapping back to a stale one. A re-entrancy flag on the tracker's own
writes prevents that ViewChanged from feeding back into a redundant reposition.

Lifecycle is fully cleaned up: SetupDiagonalScroll is wired from the control's Loaded, and
TeardownDiagonalScroll (from Unload) stops the timer, unsubscribes, and disposes the composition objects
so an editor instance never leaks a forever-running DispatcherQueueTimer. Everything is best-effort — a
composition failure just leaves the editor on wheel-only scrolling.

Changes

  • new Core/CoreTextControlBox.DiagonalScroll.cs — the IInteractionTrackerOwner partial.
  • Core/CoreTextControlBox.xaml.csLoaded += SetupDiagonalScroll and TeardownDiagonalScroll() in
    Unload.

Verification & validation

TextControlBox (library) and the test project build clean (x64). This is pure interaction glue over the
tested offset seam (#34's ScrollOffsetMath covers the offset↔scrollbar conversions), so there's no new
headless-testable math. It does need a physical precision trackpad to validate — it can't be exercised
over Remote Desktop / TeamViewer (those inject WM_MOUSEWHEEL, a single-axis wheel, which
CapableTouchpadOnly ignores, so the tracker never engages). Happy to iterate on feel (e.g. enabling inertia,
driving the scrollbar thumbs as indicators) once you've tried it.

Introduce a small IScrollOffsetSource abstraction so the editor scroll position is expressed in pixels rather than legacy scrollbar units (SingleLineHeight / DefaultVerticalScrollSensitivity). ScrollManager routes every offset read/write through it; the ScrollBarOffsetSource adapter backs it with the existing two ScrollBar primitives and does the pixel<->scrollbar-unit conversion internally (ScrollOffsetMath). Behavior is unchanged. Foundation for word-wrap scrolling, long-line horizontal virtualization and diagonal 2-axis touchpad scrolling (FrozenAssassine#33). Adds ScrollOffsetMathTests.
On files with pathologically long lines (minified JS/CSS/JSON, JSONL logs), the renderer joined every visible line into one multi-megabyte string and laid it out on every frame, causing horizontal-scroll stutter. TextRenderer.Draw now lays out only a sliced window of each visible line when a visible line exceeds 50k chars; ordinary files keep the untouched render path. The uniform slice window is re-aligned to document coordinates via HorizontalOffset + a per-line prefix sum, and the caret, click hit-testing and selection map through GetRenderedCharacterIndexForDocumentCharacter / GetDocumentCharacterIndexFromRenderedIndex / GetRenderedLayoutIndexForDocument (all no-ops when inactive). The window/index arithmetic is extracted into a WinUI-free HorizontalSliceMath with unit tests. Step 2 of FrozenAssassine#33.
A composition InteractionTracker + VisualInteractionSource on the selection canvas reads the raw 2-D precision-touchpad manipulation delta and drives the pixel IScrollOffsetSource, enabling free diagonal panning that the single-axis wheel cannot express. Configured CapableTouchpadOnly so mouse wheel / Shift+wheel / Ctrl+wheel zoom / click / drag-select are untouched. Follows programmatic scrolls immediately via the offset source's ViewChanged (with a re-entrancy guard on its own writes), and is fully torn down in Unload so no timer/composition objects leak. Step 4 of FrozenAssassine#33.
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.

1 participant