Skip to content

fix: prevent carousel touch loss during pagination#1127

Merged
utkarshdalal merged 3 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:fix/carousel-touch-lost-after-pagination
Apr 8, 2026
Merged

fix: prevent carousel touch loss during pagination#1127
utkarshdalal merged 3 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:fix/carousel-touch-lost-after-pagination

Conversation

@xXJSONDeruloXx
Copy link
Copy Markdown
Contributor

@xXJSONDeruloXx xXJSONDeruloXx commented Apr 7, 2026

Description

when you scroll to the end and pagination fires (new items appended, triggering recomposition), the draggable modifier's drag session gets reset, dropping the in-flight touch gesture.

Fix: Touch events now flow exclusively through LazyRow's own native scrolling, which is stable across pagination recomps.

Discord report: https://discord.com/channels/1378308569287622737/1490021951224287443

Recording

screen-20260407-082626.mp4

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by CodeRabbit

  • Bug Fixes
    • Improved carousel mouse dragging: dragging now starts only after a small movement threshold, produces smoother horizontal scrolling, and correctly stops on release or exit; scroll-wheel scrolling behavior remains intact.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Replaced Compose Foundation draggable-based horizontal handling in LibraryCarouselPane with a custom pointerInput implementation that tracks mouse press/move/release, applies a pixel slop before dispatching horizontal deltas to listState.dispatchRawDelta(-delta), and confines drag handling to PointerType.Mouse.

Changes

Cohort / File(s) Summary
Mouse Drag Input Refactor
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt
Removed draggable + rememberDraggableState. Added Modifier.carouselMouseInput using pointerInput to track press, pixel slop, dragging state, lastDragX, and dispatch horizontal deltas via listState.dispatchRawDelta(-delta). Wheel/scroll behavior retained; pointer filtering uses PointerType.Mouse.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

🐰 I nibble code where cursors glide,
I wait for slop before I slide.
Left and right the pixels hop,
Wheels whisper on — I never stop,
A tiny rabbit steers the ride. 🎠

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix (preventing carousel touch loss) and its context (pagination), directly matching the primary change in the changeset.
Description check ✅ Passed The description provides context about the problem, explains the fix, links to the Discord report, includes a recording, and completes all checklist items as required by the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@xXJSONDeruloXx xXJSONDeruloXx marked this pull request as draft April 7, 2026 12:21
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt (1)

99-136: ⚠️ Potential issue | 🔴 Critical

Handle PointerEventType.Exit and remove unnecessary launch wrapper on dispatchRawDelta.

The code does not clear isDragging when the pointer exits the region. If the user drags, then moves the mouse away from the carousel while still holding the button, isDragging remains true and subsequent Move events process with stale lastDragX, causing unintended scroll deltas. Additionally, dispatchRawDelta is a non-suspend function, so wrapping it in launch {} is incorrect and unnecessary—call it directly in the coroutineScope.

Also address the mismatch: isMouseEvent checks if any change is a mouse, but firstOrNull()?.position may read a different pointer type. Extract the mouse change directly and read its position.

Suggested fix
-                    val isMouseEvent = event.changes.any { it.type == PointerType.Mouse }
+                    val mouseChange = event.changes.firstOrNull { it.type == PointerType.Mouse }
                     when (event.type) {
                         PointerEventType.Scroll -> {
                             val scrollDelta = event.changes.firstOrNull()?.scrollDelta
@@ -116,11 +116,11 @@
                         PointerEventType.Press -> {
-                            if (isMouseEvent) {
-                                lastDragX = event.changes.firstOrNull()?.position?.x ?: 0f
+                            if (mouseChange?.pressed == true) {
+                                lastDragX = mouseChange.position.x
                                 isDragging = true
                             }
                         }
                         PointerEventType.Move -> {
-                            if (isDragging && isMouseEvent) {
-                                val currentX = event.changes.firstOrNull()?.position?.x ?: lastDragX
+                            if (isDragging && mouseChange != null) {
+                                val currentX = mouseChange.position.x
                                 val delta = currentX - lastDragX
                                 lastDragX = currentX
                                 if (delta != 0f) {
-                                    launch { listState.dispatchRawDelta(-delta) }
+                                    listState.dispatchRawDelta(-delta)
                                 }
                             }
                         }
@@ -134,6 +134,9 @@
                         PointerEventType.Release -> {
                             isDragging = false
                         }
+                        PointerEventType.Exit -> {
+                            isDragging = false
+                        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt`
around lines 99 - 136, Handle PointerEventType.Exit by setting isDragging =
false to avoid stale drag state; stop wrapping dispatchRawDelta in launch (call
listState.dispatchRawDelta(...) directly since it’s non-suspending). Also
replace uses of event.changes.firstOrNull() with a dedicated mouseChange =
event.changes.firstOrNull { it.type == PointerType.Mouse } and read
mouseChange.position/scrollDelta so you only read positions from the actual
mouse pointer; apply this to the Scroll and Move branches and use dominantDelta
from mouseChange.scrollDelta.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt`:
- Around line 99-136: Handle PointerEventType.Exit by setting isDragging = false
to avoid stale drag state; stop wrapping dispatchRawDelta in launch (call
listState.dispatchRawDelta(...) directly since it’s non-suspending). Also
replace uses of event.changes.firstOrNull() with a dedicated mouseChange =
event.changes.firstOrNull { it.type == PointerType.Mouse } and read
mouseChange.position/scrollDelta so you only read positions from the actual
mouse pointer; apply this to the Scroll and Move branches and use dominantDelta
from mouseChange.scrollDelta.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3dbb63e2-4b15-4656-aada-ac9712746867

📥 Commits

Reviewing files that changed from the base of the PR and between 8f95184 and 7489cd0.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt:129">
P2: Per-move coroutine launching in drag handling adds avoidable scheduling overhead in a high-frequency input loop and can cause less responsive/jittery scrolling.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@xXJSONDeruloXx xXJSONDeruloXx marked this pull request as ready for review April 7, 2026 13:04
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt`:
- Around line 116-130: The drag slop is missing: change the press/drag logic in
the PointerEventType.Press/Move handling so a separate "pressed" state (e.g.,
pressedStartX or isPressed) records the initial press X but does not set
isDragging immediately; on PointerEventType.Move compute horizontal distance
from pressedStartX and only set isDragging = true and set lastDragX when the
absolute delta exceeds a small threshold (e.g., a few pixels), and until
promoted ignore small moves (do not call listState.dispatchRawDelta). Use the
existing symbols mouseChange, lastDragX, isDragging, listState.dispatchRawDelta,
and promote the pressed state to active drag once threshold is crossed, updating
lastDragX to the currentX at that moment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4073f487-e991-4137-bf7d-367e40951c96

📥 Commits

Reviewing files that changed from the base of the PR and between 7489cd0 and e522655.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt (1)

144-147: Mouse exit may leave carousel mid-scroll without snapping.

When the mouse exits the carousel during a drag (PointerEventType.Exit), the drag state resets but dispatchRawDelta leaves the scroll position wherever it stopped. The SnapFlingBehavior on LazyRow only triggers on fling gestures, not on abrupt drag termination.

Consider triggering a programmatic scroll-to-nearest-item on exit to maintain the carousel's snapped appearance, or let the existing flingBehavior handle it by not resetting on Exit (only on Release).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt`
around lines 144 - 147, PointerEventType.Exit currently clears isDragging and
pressedStartX but leaves the scroll mid-position because dispatchRawDelta
doesn't snap; update the pointer handling so Exit either doesn't reset the drag
state (only reset on PointerEventType.Release) or, when handling
PointerEventType.Exit, launch a coroutine that computes the nearest item from
lazyListState (using firstVisibleItemIndex and firstVisibleItemScrollOffset) and
programmatically scrolls/animates to that index (e.g., animateScrollToItem /
scrollToItem) to enforce snapping; reference the onPointerEvent branch handling
PointerEventType.Exit, isDragging, pressedStartX, dispatchRawDelta,
SnapFlingBehavior, and lazyListState to locate where to add the coroutine-driven
snap or change the reset behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt`:
- Around line 144-147: PointerEventType.Exit currently clears isDragging and
pressedStartX but leaves the scroll mid-position because dispatchRawDelta
doesn't snap; update the pointer handling so Exit either doesn't reset the drag
state (only reset on PointerEventType.Release) or, when handling
PointerEventType.Exit, launch a coroutine that computes the nearest item from
lazyListState (using firstVisibleItemIndex and firstVisibleItemScrollOffset) and
programmatically scrolls/animates to that index (e.g., animateScrollToItem /
scrollToItem) to enforce snapping; reference the onPointerEvent branch handling
PointerEventType.Exit, isDragging, pressedStartX, dispatchRawDelta,
SnapFlingBehavior, and lazyListState to locate where to add the coroutine-driven
snap or change the reset behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0bc62a3-d1f3-488f-9d4b-81ae833f484f

📥 Commits

Reviewing files that changed from the base of the PR and between e522655 and 2067720.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt

Copy link
Copy Markdown
Contributor

@phobos665 phobos665 left a comment

Choose a reason for hiding this comment

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

LGTM

@utkarshdalal utkarshdalal merged commit d25eaa6 into utkarshdalal:master Apr 8, 2026
3 checks passed
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.

3 participants