Fix gesture hit-testing for transformed controls in virtualized/cached scrolls#303
Open
LennoxP90 wants to merge 1 commit into
Open
Conversation
…d scrolls A control with a transform (ScaleX/ScaleY, Rotation*, skew, flip) renders correctly but pointer events never reach its children — the transform is applied to rendering but ignored by gesture hit-testing. This breaks the standard inverted-list pattern (a ScaleY=-1 SkiaScroll with counter-flipped cells) inside virtualization: cells render upright, but taps on links/buttons inside a cell do nothing. Three causes, all in gesture hit-testing (no rendering changes): - RenderTransformMatrix is stale at gesture time — ApplyTransforms stores it only when !NodeAttached, and a virtualized control's matrix pivot (built from DrawingRect at render time) goes stale as it scrolls. Add EnsureRenderTransformMatrixForGestures() to recompute it from the current DrawingRect for transformed controls, called before the inverse-map in ProcessGestures and in IsGestureForChild. - The virtualized planes path (SkiaScroll.ProcessGesturesForPlane) bypasses the entry inverse-map and hit-tests/forwards raw coords. Map the gesture point and the forwarded MappedLocation by the scroll's transform. - The registered-listener branch of SkiaControl.ProcessGestures and SkiaLabel.ProcessGestures (span/link hit-testing) used the raw args.Event.Location instead of the entry-mapped MappedLocation. All changes are gated on HasTransform (or are identical to prior behaviour when nothing in the ancestry is transformed, since MappedLocation == args.Event.Location in that case), so non-transformed lists take byte-identical code paths. No features removed, no public API change beyond one helper. Verified on Android with an inverted virtualized SkiaScroll (link taps now fire; removing the transforms confirms the link was already tappable).
LennoxP90
added a commit
to LennoxP90/DrawnUi
that referenced
this pull request
Jun 4, 2026
Minimal fork patches enabling WFMC's chat-detail virtualized list with RecyclingTemplate=Enabled (bounded-memory recycling) + MeasureVisible: 1. ViewsAdapter.InitializeSoft - refresh the immutable render snapshot on incremental collection change. It was only rebuilt on an ItemsSource REFERENCE change; appends to the same ObservableCollection left it frozen at the old length, so GetViewForIndex could not realise appended indices (the "only the first page renders" pagination bug). 2. SkiaLayout.ApplyAddChange (MeasureVisible, add-at-end) - re-kick background measurement from LastMeasuredIndex+1. The one-shot task idles after the initial set, so appended (LoadMore) rows were never measured. Cancel first so a stale still-flagged task does not dedup-skip the restart. 3. SkiaDrawnCell.ApplyBindingContext - re-measure on recycle (WasMeasured=false + InvalidateMeasureInternal). SetContent runs inside LockUpdate, suppressing its re-measure, so a recycled cell kept the donor message width (variable-width bubbles rendered wrong). Forces re-measure for the new content. Verified emulator-5554, 914-msg thread: correct render + bounded memory (~645MB@914, flat on re-scroll; vs ~1.3GB grow-only). Carries PRs taublast#301/taublast#303. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR: Fix gesture hit-testing for transformed controls in virtualized/cached scrolls
Fixes #302.
Problem
A control with a transform (
ScaleX/ScaleY,Rotation*, skew, flip) renders correctly but pointerevents never reach its children — the transform is applied to rendering but ignored by gesture
hit-testing. This breaks the standard inverted-list pattern (a
ScaleY="-1"SkiaScrollwithcounter-flipped cells for a newest-at-bottom chat list) inside virtualization: cells render upright,
but taps on links/buttons inside a cell do nothing. See the linked issue for the full root-cause.
Fix
Gesture hit-testing only — no rendering changes:
SkiaControl.EnsureRenderTransformMatrixForGestures()(new): recomputesRenderTransformMatrixfrom the current
DrawingRectfor transformed controls. Called before the inverse-map inProcessGesturesand inIsGestureForChild. Fixes the stale/Identity matrix on node-attached/cachedcontrols and the stale pivot on virtualized controls (whose
DrawingRectmoves as they scroll).SkiaScroll.ProcessGesturesForPlane: map the gesture point and the forwardedMappedLocationby the scroll's transform (the virtualized planes path overrides
ProcessGesturesand never reachesthe base entry inverse-map).
SkiaControl.ProcessGestures(registered-listener branch): hit-test transformed controls withthe entry-mapped
MappedLocation(consistent with the rendering-tree branch); the non-transformedpath is kept exactly as before.
SkiaLabel.ProcessGestures: span (link) hit-testing uses the entry-mappedMappedLocationinstead of the raw
args.Event.Location.Three files:
SkiaControl.Shared.cs,SkiaScroll.Planes.cs,SkiaLabel.cs.No regression for non-transformed UI
Changes 1–3 are gated on
HasTransform; change 4 is identical to the previous behaviour when nothingin the ancestry is transformed, because
MappedLocation == args.Event.Locationin that case (the rootsets
MappedLocation = touchLocationand the entry inverse-map is a no-op without a transform). So anormal, non-transformed list takes byte-identical code paths.
Testing
(
ScaleY="-1") virtualizedSkiaScrollnow fires (opens the browser); without the change the tap isdetected on the cell but never reaches the label's link span.
correctly (the gated paths fall through to the original behaviour).
Notes / for discussion
EnsureRenderTransformMatrixForGestures()recomputes the matrix per gesture event for transformedcontrols (a single cheap matrix build). If preferred, this could be made dirty-tracked / cached and
invalidated on transform or layout change — happy to adjust.
mapped coordinates); this aligns them for transformed controls while leaving the common path intact.