Skip to content

Commit

Permalink
Move DefaultHardwareShortcutDetector to /internal
Browse files Browse the repository at this point in the history
  • Loading branch information
saket committed Jun 24, 2024
1 parent 21de26d commit b9c7172
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,12 @@ package me.saket.telephoto.zoomable

import androidx.compose.runtime.Immutable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.isAltPressed
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isAltPressed
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFold
import androidx.compose.ui.util.fastForEach
import dev.drewhamilton.poko.Poko
import me.saket.telephoto.zoomable.HardwareShortcutDetector.ShortcutEvent
import me.saket.telephoto.zoomable.HardwareShortcutDetector.ShortcutEvent.PanDirection
import me.saket.telephoto.zoomable.HardwareShortcutDetector.ShortcutEvent.ZoomDirection
import me.saket.telephoto.zoomable.internal.HostPlatform
import me.saket.telephoto.zoomable.internal.current
import kotlin.math.absoluteValue
import me.saket.telephoto.zoomable.internal.DefaultHardwareShortcutDetector

@Immutable
interface HardwareShortcutDetector {
Expand Down Expand Up @@ -66,86 +52,3 @@ interface HardwareShortcutDetector {
}
}
}

internal object DefaultHardwareShortcutDetector : HardwareShortcutDetector {
override fun detectKey(event: KeyEvent): ShortcutEvent? {
// Note for self: Some devices/peripherals have dedicated zoom buttons that map to Key.ZoomIn
// and Key.ZoomOut. Examples include: Samsung Galaxy Camera, a motorcycle handlebar controller.
if (event.key == Key.ZoomIn || event.isZoomInEvent()) {
return ShortcutEvent.Zoom(ZoomDirection.In)
} else if (event.key == Key.ZoomOut || (event.isZoomOutEvent())) {
return ShortcutEvent.Zoom(ZoomDirection.Out)
}

val panDirection = when (event.key) {
Key.DirectionUp -> PanDirection.Up
Key.DirectionDown -> PanDirection.Down
Key.DirectionLeft -> PanDirection.Left
Key.DirectionRight -> PanDirection.Right
else -> null
}
return when (panDirection) {
null -> null
else -> ShortcutEvent.Pan(
direction = panDirection,
panOffset = ShortcutEvent.DefaultPanOffset * if (event.isAltPressed) 10f else 1f,
)
}
}

private fun KeyEvent.isZoomInEvent(): Boolean {
return this.key == Key.Equals && when (HostPlatform.current) {
HostPlatform.Android -> isCtrlPressed
HostPlatform.Desktop -> isMetaPressed
}
}

private fun KeyEvent.isZoomOutEvent(): Boolean {
return key == Key.Minus && when (HostPlatform.current) {
HostPlatform.Android -> isCtrlPressed
HostPlatform.Desktop -> isMetaPressed
}
}

override fun detectScroll(event: PointerEvent): ShortcutEvent? {
if (!event.keyboardModifiers.isAltPressed) {
// Google Photos does not require any modifier key to be pressed for zooming into
// images using mouse scroll. Telephoto does not follow the same pattern because
// it might migrate to 2D scrolling in the future for panning content once Compose
// UI supports it.
return null
}
return when (val scrollY = event.calculateScroll().y) {
0f -> null
else -> ShortcutEvent.Zoom(
direction = if (scrollY < 0f) ZoomDirection.In else ZoomDirection.Out,
centroid = event.calculateScrollCentroid(),
// Scroll delta always seems to be either 1f or -1f depending on the direction.
// Although some mice are capable of sending precise scrolls, I'm assuming
// Android coerces them to be at least (+/-)1f.
zoomFactor = ShortcutEvent.DefaultZoomFactor * scrollY.absoluteValue,
)
}
}

private fun PointerEvent.calculateScroll(): Offset {
return changes.fastFold(Offset.Zero) { acc, c ->
acc + c.scrollDelta
}
}

private fun PointerEvent.calculateScrollCentroid(): Offset {
check(type == PointerEventType.Scroll)
var centroid = Offset.Zero
var centroidWeight = 0f
changes.fastForEach { change ->
val position = change.position
centroid += position
centroidWeight++
}
return when (centroidWeight) {
0f -> Offset.Unspecified
else -> centroid / centroidWeight
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package me.saket.telephoto.zoomable.internal

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.isAltPressed
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isAltPressed
import androidx.compose.ui.util.fastFold
import androidx.compose.ui.util.fastForEach
import me.saket.telephoto.zoomable.HardwareShortcutDetector
import kotlin.math.absoluteValue

internal object DefaultHardwareShortcutDetector : HardwareShortcutDetector {
override fun detectKey(event: KeyEvent): HardwareShortcutDetector.ShortcutEvent? {
// Note for self: Some devices/peripherals have dedicated zoom buttons that map to Key.ZoomIn
// and Key.ZoomOut. Examples include: Samsung Galaxy Camera, a motorcycle handlebar controller.
if (event.key == Key.ZoomIn || event.isZoomInEvent()) {
return HardwareShortcutDetector.ShortcutEvent.Zoom(HardwareShortcutDetector.ShortcutEvent.ZoomDirection.In)
} else if (event.key == Key.ZoomOut || (event.isZoomOutEvent())) {
return HardwareShortcutDetector.ShortcutEvent.Zoom(HardwareShortcutDetector.ShortcutEvent.ZoomDirection.Out)
}

val panDirection = when (event.key) {
Key.DirectionUp -> HardwareShortcutDetector.ShortcutEvent.PanDirection.Up
Key.DirectionDown -> HardwareShortcutDetector.ShortcutEvent.PanDirection.Down
Key.DirectionLeft -> HardwareShortcutDetector.ShortcutEvent.PanDirection.Left
Key.DirectionRight -> HardwareShortcutDetector.ShortcutEvent.PanDirection.Right
else -> null
}
return when (panDirection) {
null -> null
else -> HardwareShortcutDetector.ShortcutEvent.Pan(
direction = panDirection,
panOffset = HardwareShortcutDetector.ShortcutEvent.DefaultPanOffset * if (event.isAltPressed) 10f else 1f,
)
}
}

private fun KeyEvent.isZoomInEvent(): Boolean {
return this.key == Key.Equals && when (HostPlatform.current) {
HostPlatform.Android -> isCtrlPressed
HostPlatform.Desktop -> isMetaPressed
}
}

private fun KeyEvent.isZoomOutEvent(): Boolean {
return key == Key.Minus && when (HostPlatform.current) {
HostPlatform.Android -> isCtrlPressed
HostPlatform.Desktop -> isMetaPressed
}
}

override fun detectScroll(event: PointerEvent): HardwareShortcutDetector.ShortcutEvent? {
if (!event.keyboardModifiers.isAltPressed) {
// Google Photos does not require any modifier key to be pressed for zooming into
// images using mouse scroll. Telephoto does not follow the same pattern because
// it might migrate to 2D scrolling in the future for panning content once Compose
// UI supports it.
return null
}
return when (val scrollY = event.calculateScroll().y) {
0f -> null
else -> HardwareShortcutDetector.ShortcutEvent.Zoom(
direction = if (scrollY < 0f) HardwareShortcutDetector.ShortcutEvent.ZoomDirection.In else HardwareShortcutDetector.ShortcutEvent.ZoomDirection.Out,
centroid = event.calculateScrollCentroid(),
// Scroll delta always seems to be either 1f or -1f depending on the direction.
// Although some mice are capable of sending precise scrolls, I'm assuming
// Android coerces them to be at least (+/-)1f.
zoomFactor = HardwareShortcutDetector.ShortcutEvent.DefaultZoomFactor * scrollY.absoluteValue,
)
}
}

private fun PointerEvent.calculateScroll(): Offset {
return changes.fastFold(Offset.Zero) { acc, c ->
acc + c.scrollDelta
}
}

private fun PointerEvent.calculateScrollCentroid(): Offset {
check(type == PointerEventType.Scroll)
var centroid = Offset.Zero
var centroidWeight = 0f
changes.fastForEach { change ->
val position = change.position
centroid += position
centroidWeight++
}
return when (centroidWeight) {
0f -> Offset.Unspecified
else -> centroid / centroidWeight
}
}
}

0 comments on commit b9c7172

Please sign in to comment.