I have a desktop application in which i want to implement zooming and panning behaviour, like in this question, but without scrollbars. However, i don't know how to allow the user to wheel-zoom on an exact point. I'm using Kotlin Compose Multiplatform 1.4.3
Panning works fine, but offset is calculated incorrectly after scaling. The mouse cursor should stay in the same position after zooming.
Here is my code:
fun main() = singleWindowApplication {
Surface {
Box {
PanAndZoom(
modifier = Modifier
.fillMaxSize()
) {
Box(modifier = Modifier.size(300.dp).background(Color.Gray))
}
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PanAndZoom(
modifier: Modifier = Modifier,
state: PanAndZoomState = remember { PanAndZoomState() },
content: @Composable BoxScope.() -> Unit
) {
Box(
modifier = modifier
.onPointerEvent(PointerEventType.Scroll) {
val change = it.changes.first()
val delta = change.scrollDelta.y.toInt().sign
val position = change.position
val posBeforeZoom = state.screenToWorld(position)
// Zooming
state.scale(delta)
val posAfterZoom = state.screenToWorld(position)
// Incorrect offset calculation
state.offset += (posBeforeZoom - posAfterZoom)
}
.pointerInput(Unit) {
// Panning
detectDragGestures { _, dragAmount ->
state.offset += dragAmount
}
}
.onGloballyPositioned {
state.size = it.size
}
) {
Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer {
with(state) {
// Applying transformations
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
}
},
contentAlignment = Alignment.Center,
content = content
)
}
}
class PanAndZoomState {
var size: IntSize = IntSize.Zero
var offset by mutableStateOf(Offset.Zero)
var scale by mutableStateOf(1f)
fun scale(delta: Int) {
scale = (scale * exp(delta * 0.2f)).coerceIn(0.25f, 1.75f)
}
fun screenToWorld(screenOffset: Offset): Offset {
return (screenOffset * scale) + ((size.toOffset() * (1 - scale) ) / 2f)
}
}
fun IntSize.toOffset(): Offset {
return Offset(
x = width.toFloat(),
y = height.toFloat()
)
}