Skip to content

Add onTap callback to ZoomablePlugin (#863)#940

Merged
skydoves merged 1 commit into
mainfrom
fix/863-zoomable-tap-callback
Jul 2, 2026
Merged

Add onTap callback to ZoomablePlugin (#863)#940
skydoves merged 1 commit into
mainfrom
fix/863-zoomable-tap-callback

Conversation

@skydoves

@skydoves skydoves commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #863. When ZoomablePlugin is enabled, its double-tap detector consumes the pointer down on every tap, so a parent Modifier.clickable never fires. This is inherent to detectTapGestures (it calls down.consume() as soon as a pointer goes down, regardless of whether the gesture becomes a double tap), and the parent's clickable requires an unconsumed down. A single tap therefore cannot both drive zoom and bubble to a parent.

This PR exposes an onTap callback so taps can be handled at the plugin level, which is what the issue asks for ("provide a click callback within the ZoomablePlugin").

Changes

  • Add ZoomablePlugin.onTap: ((Offset) -> Unit)? and thread it through ZoomableContent (android/skia), SubSamplingImage, and zoomGestures.
  • Install the tap detector when double-tap zoom OR onTap is set, and make onDoubleTap conditional on enableDoubleTapZoom. When both are off, no tap detector is installed, so a single tap stays unconsumed and reaches a parent gesture handler (previously the only workaround).
  • Fix a stale KDoc example that referenced a nonexistent ZoomablePlugin(config = ...) parameter; configuration is supplied via rememberZoomableState(config = ...).

The change is source-compatible: onTap defaults to null, and with it unset behavior is identical to before.

Usage

GlideImage(
  imageModel = { imageUrl },
  component = rememberImageComponent {
    +ZoomablePlugin(onTap = { position -> openFullScreen() })
  },
)

Tests

Adds desktop runComposeUiTest coverage in ZoomGesturesTest (runs on the JVM via desktopTest, no emulator):

  • doubleTap_zoomsIn_withDefaultConfig and doubleTap_whenZoomed_resetsZoom — regression: existing double-tap zoom/reset still works.
  • singleTap_invokesOnTapCallback — new: a single tap fires onTap.
  • singleTap_invokesOnTap_notParentClickable — new: onTap fires while the parent clickable stays silent (documents the intended replacement).
  • singleTap_reachesParent_whenTapHandlingDisabled — regression: with tap handling off, a single tap still bubbles to the parent.

All 5 pass locally. All targets (android/desktop/ios/macos/wasm) compile.

Notes

  • Adds org.jetbrains.compose.ui:ui-test-junit4 and the host skiko-awt-runtime (classifier detected per host, so CI/Linux works) as desktopTest-only dependencies.
  • Test compiles were excluded from explicit-API mode in the module build, matching landscapist-core.

Summary by CodeRabbit

  • New Features
    • Added optional single-tap handling to zoomable content, so apps can react to taps while zoom gestures are enabled.
    • Improved desktop UI test support across host environments and added compose UI test libraries.
  • Bug Fixes
    • Double-tap zoom behavior is now covered by tests, including reset behavior when zoomed in.
    • Tap handling now correctly prevents parent click targets from receiving taps when zoom gestures consume them.
  • Tests
    • Added desktop Compose UI tests for tap and zoom interaction behavior.

When ZoomablePlugin is enabled, its double-tap detector consumes the
pointer down on every tap, so a parent Modifier.clickable never fires.
That is inherent to detectTapGestures (it consumes the down regardless
of whether the tap becomes a double tap), so a tap cannot both zoom the
image and bubble to a parent.

Expose an onTap callback so taps can be handled at the plugin level
instead of relying on the parent:

- Add ZoomablePlugin.onTap and thread it through ZoomableContent
  (android/skia), SubSamplingImage, and zoomGestures.
- Install the tap detector when double-tap zoom OR onTap is set, and make
  onDoubleTap conditional on enableDoubleTapZoom. When both are off no tap
  detector is installed, so a single tap stays unconsumed and reaches a
  parent gesture handler (previously the only workaround).
- Fix a stale KDoc example that referenced a nonexistent
  ZoomablePlugin(config = ...) parameter.

Adds desktop runComposeUiTest coverage: double-tap still zooms and
resets (regression), single tap invokes onTap, onTap fires while the
parent clickable stays silent, and the tap-handling-disabled path still
reaches the parent.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds an optional onTap callback to ZoomablePlugin, threading it through ZoomableContent (Android and Skia targets), SubSamplingImage, and ZoomGestureDetector so single taps can be handled without conflicting with double-tap zoom. New desktop UI tests validate tap/zoom/click interactions, and Gradle build files add Skiko runtime dependencies for desktop testing.

Changes

onTap callback feature

Layer / File(s) Summary
Public API and expect/actual contract
ZoomablePlugin.kt, ZoomableContent.kt (commonMain)
ZoomablePlugin gains an onTap: ((Offset) -> Unit)? constructor parameter with updated KDoc/examples, forwarded via compose() into the ZoomableContent expect declaration.
Tap gesture detection
ZoomGestureDetector.kt
zoomGestures installs pointer input when double-tap zoom is enabled or onTap is provided; single taps invoke onTap while double-tap zoom toggling is preserved.
SubSamplingImage wiring
SubSamplingImage.kt
Adds onTap parameter, forwarding it into Modifier.zoomGestures.
Android ZoomableContent wiring
ZoomableContent.android.kt
Threads onTap through SubSamplingImageWithPlaceholder, SubSamplingImage, and StandardZoomableContent.
Skia ZoomableContent wiring
ZoomableContent.skia.kt
Threads onTap through the equivalent Skia/desktop rendering paths.
Desktop UI tests
ZoomGesturesTest.kt
New ZoomGesturesTest suite verifies double-tap zoom/reset, onTap invocation, and that tap handling blocks or allows parent clickable depending on configuration.
Build configuration
gradle/libs.versions.toml, landscapist-zoomable/build.gradle.kts
Adds skiko version and Compose UI test library aliases, a skikoHostTarget() helper with host-specific skiko-awt-runtime dependency for desktopTest, and restricts -Xexplicit-api=strict to non-test compilation tasks.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ZoomGestureDetector
    participant ZoomableState
    participant ZoomablePlugin
    participant ParentClickable

    User->>ZoomGestureDetector: single tap
    alt onTap provided or double-tap zoom enabled
        ZoomGestureDetector->>ZoomGestureDetector: detectTapGestures consumes pointer down
        ZoomGestureDetector->>ZoomablePlugin: invoke onTap(position)
        Note over ParentClickable: does not receive tap
    else no tap handling configured
        ZoomGestureDetector-->>ParentClickable: pointer event propagates
        ParentClickable->>ParentClickable: handle click
    end

    User->>ZoomGestureDetector: double tap
    ZoomGestureDetector->>ZoomableState: toggle zoom / reset
Loading

Possibly related PRs

  • skydoves/landscapist#805: Originally introduced ZoomablePlugin and the zoom/sub-sampling pipeline that this PR extends with onTap.
  • skydoves/landscapist#807: Introduced the subsampling/placeholder decoding flow in ZoomableContent.skia.kt that this PR now threads onTap through.

Poem

A tap, a hop, a gentle press,
No more clicks lost in zoom's excess!
With onTap now the parent hears,
The single taps, the double smears 🐰
Skiko runs on desktop true,
Tests all pass — a bunny's cheer for you! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed It clearly states the main change: adding an onTap callback to ZoomablePlugin, which matches the PR's core feature.
Description check ✅ Passed The description covers the goal, implementation, usage, tests, and review notes, though it doesn't follow the repo's exact template headings.
Linked Issues check ✅ Passed [#863] The PR adds an onTap callback, threads it through gesture handling, and tests the clickable/tap behavior described in the issue.
Out of Scope Changes check ✅ Passed The Gradle and test-runtime changes support the new UI tests and API updates, so no unrelated code changes stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/863-zoomable-tap-callback

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
landscapist-zoomable/src/desktopTest/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomGesturesTest.kt (1)

110-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a default-config coverage case.

Tests cover onTap-enabled (consumes tap, parent doesn't fire) and tap-handling-fully-disabled (parent fires) paths well. Missing is the original issue #863 scenario with default config (enableDoubleTapZoom=true, no onTap) verifying the tap is still consumed by the detector without crashing and without reaching a parent clickable — this is the exact bug the PR references.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@landscapist-zoomable/src/desktopTest/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomGesturesTest.kt`
around lines 110 - 169, Add a test in ZoomGesturesTest covering the default
ZoomableConfig path with no onTap, since this is the `#863` scenario. Reuse
zoomGestures, rememberZoomableState, and the parent clickable setup to verify a
single tap is still consumed by the gesture detector, does not crash, and does
not increment the parent click count. Place it alongside
singleTap_invokesOnTap_notParentClickable and
singleTap_reachesParent_whenTapHandlingDisabled for clear coverage of the three
tap-handling modes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@landscapist-zoomable/src/commonMain/kotlin/com/skydoves/landscapist/zoomable/ZoomablePlugin.kt`:
- Around line 63-65: The tap-consumption note in ZoomablePlugin is too broad
because zoomGestures now bypasses the tap detector when both enableDoubleTapZoom
and onTap are disabled. Update the KDoc around ZoomablePlugin and the related
wording near zoomGestures to say parent Modifier.clickable is blocked only while
tap handling is active, and keep the guidance aligned with onTap being the
replacement for single-tap handling.

---

Nitpick comments:
In
`@landscapist-zoomable/src/desktopTest/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomGesturesTest.kt`:
- Around line 110-169: Add a test in ZoomGesturesTest covering the default
ZoomableConfig path with no onTap, since this is the `#863` scenario. Reuse
zoomGestures, rememberZoomableState, and the parent clickable setup to verify a
single tap is still consumed by the gesture detector, does not crash, and does
not increment the parent click count. Place it alongside
singleTap_invokesOnTap_notParentClickable and
singleTap_reachesParent_whenTapHandlingDisabled for clear coverage of the three
tap-handling modes.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 41bf825d-84f5-4468-8f6d-dbe19f461993

📥 Commits

Reviewing files that changed from the base of the PR and between 8592603 and c3e72a3.

📒 Files selected for processing (9)
  • gradle/libs.versions.toml
  • landscapist-zoomable/build.gradle.kts
  • landscapist-zoomable/src/androidMain/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomableContent.android.kt
  • landscapist-zoomable/src/commonMain/kotlin/com/skydoves/landscapist/zoomable/ZoomablePlugin.kt
  • landscapist-zoomable/src/commonMain/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomGestureDetector.kt
  • landscapist-zoomable/src/commonMain/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomableContent.kt
  • landscapist-zoomable/src/commonMain/kotlin/com/skydoves/landscapist/zoomable/subsampling/SubSamplingImage.kt
  • landscapist-zoomable/src/desktopTest/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomGesturesTest.kt
  • landscapist-zoomable/src/skiaMain/kotlin/com/skydoves/landscapist/zoomable/internal/ZoomableContent.skia.kt

Comment on lines +63 to +65
* **Handling taps:** because the zoom gesture detector consumes the pointer down, a parent
* `Modifier.clickable` will not receive taps while zoom gestures are enabled. Use [onTap] to react
* to single taps instead:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Narrow the tap-consumption wording.

Line 64 and Line 93 say parent clickable is blocked whenever zoom gestures are enabled, but zoomGestures now skips the tap detector when both enableDoubleTapZoom and onTap are disabled. Please document this as “while tap handling is active” to avoid contradicting the pass-through behavior.

📝 Proposed wording adjustment
- * **Handling taps:** because the zoom gesture detector consumes the pointer down, a parent
- * `Modifier.clickable` will not receive taps while zoom gestures are enabled. Use [onTap] to react
- * to single taps instead:
+ * **Handling taps:** when tap handling is active (for example, [onTap] is set or
+ * double-tap zoom is enabled), the zoom gesture detector consumes the pointer down. A parent
+ * `Modifier.clickable` will not receive those taps. Use [onTap] to react to single taps instead:
...
- *   on the image, since the zoom gesture detector consumes the pointer down and a parent
- *   `Modifier.clickable` therefore does not receive the tap while zoom gestures are enabled.
+ *   on the image while tap handling is active, since the zoom gesture detector consumes the
+ *   pointer down and a parent `Modifier.clickable` therefore does not receive the tap.

Also applies to: 91-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@landscapist-zoomable/src/commonMain/kotlin/com/skydoves/landscapist/zoomable/ZoomablePlugin.kt`
around lines 63 - 65, The tap-consumption note in ZoomablePlugin is too broad
because zoomGestures now bypasses the tap detector when both enableDoubleTapZoom
and onTap are disabled. Update the KDoc around ZoomablePlugin and the related
wording near zoomGestures to say parent Modifier.clickable is blocked only while
tap handling is active, and keep the guidance aligned with onTap being the
replacement for single-tap handling.

@skydoves skydoves merged commit d57687e into main Jul 2, 2026
3 of 4 checks passed
@skydoves skydoves deleted the fix/863-zoomable-tap-callback branch July 2, 2026 01:00
@professorDeveloper

Copy link
Copy Markdown

@skydoves Hello bro, sorry for the disturbance. coderabbitai is this free tool?

@skydoves

skydoves commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Hey @professorDeveloper, yes, it's completely free for open-source projects :)

@professorDeveloper

Copy link
Copy Markdown

Hey @professorDeveloper, yes, it's completely free for open-source projects :)

Oh thanks, bro. I'm a really big fan of you 🔥

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.

Event Consumption Conflict between ZoomablePlugin and Parent Container

2 participants