Hey guys, what code do you use for scroll wheel zo...
# compose-desktop
a
Hey guys, what code do you use for scroll wheel zooming? This is what I have but it's not perfect: It seems compose-jb uses getPreciseWheelRotation from the native event but that has some odd values 0.11/-0.11 or I'm not sure how to process it. So I'm wondering how you guys handle scroll wheel zooming 🙂 Also: I'm hoping for zoom gesture support... zooming is pretty important for my use case (graphics editor app)
m
Have you tried using
Copy code
.pointerInput(Unit) {
    detectTransformGestures { centroid, pan, zoom, rotation -> 
       ...
    }
}
instead of fiddling directly with the AWT events? And in addition to that
Copy code
.pointerInput(Unit) {
    awaitPointerEventScope {
        while (true) {
            val event = awaitPointerEvent()
            ...
        }
    }
}
o
detectTransformGestures
doesn’t work for desktop, because multitouch is not supported
I’m using something like this:
Copy code
var scale by mutableStateOf(1f)
    val zoomScrollState = rememberScrollableState {
        scale = scale + scale * it / 100
        it
    }
and then
Modifier.scrollable(zoomScrollState, Orientation.Vertical)
It has a bit more code, because zooming in/out should retain point under mouse to stay under mouse, but for just scale it’s this
d
Will there be support for multitouch on desktop? Right now I use buttons for zoom in and zoom out because my use case is to use a touch screen monitor
i
Will there be support for multitouch on desktop
Yes, we plan to support true touch (with multitouch) and trackpad in some point in the future (but we haven't decided yet when we support them). Currently we rely on the platform conversion of touch events into mouse events, and it can lead to some glitches (weird scrolling, false clicks, etc).
👀 1
a
@orangy Thank you, I tried your solution and it works 🙂 however, I need mouse position in order to zoom around where the mouse cursor is. Do you know how to get mouse position inside the rememberScrollableState block?
o
You need to track mouse position separately:
Copy code
.onPointerEvent(PointerEventType.Move) { event ->
                val change = event.changes.firstOrNull()
                state.mousePosition = change?.position ?: Offset.Zero
}
Something like this. Then you have to do some math to update zoom and pan values to maintain zoom target
Here the component I have for my needs of zooming and panning
Copy code
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun Canvas(
    modifier: Modifier = Modifier,
    state: CanvasState = rememberCanvasState(),
    draw: Canvas.(Rect) -> Unit
) {
    val panX = rememberScrollableState {
        state.offset += Offset(it, 0f)
        it
    }
    val panY = rememberScrollableState {
        state.offset += Offset(0f, it)
        it
    }
    val zoomScrollState = rememberScrollableState {
        state.zoom(state.scale + state.scale * it / 100, state.mousePosition)
        it
    }

    val scrollMode by derivedStateOf {
        when {
            !App.metaKeyDown -> Modifier
                .scrollable(panX, Orientation.Horizontal)
                .scrollable(panY, Orientation.Vertical)

            else -> Modifier.scrollable(zoomScrollState, Orientation.Vertical)
        }
    }

    Box(
        modifier
            .then(scrollMode)
            .onSizeChanged { state.size = it }
            .onPointerEvent(PointerEventType.Move) { event ->
                val change = event.changes.firstOrNull()
                state.mousePosition = change?.position ?: Offset.Zero
                if (event.buttons.isSecondaryPressed) {
                    var diff = Offset.Zero
                    event.changes.forEach {
                        diff += it.position - it.previousPosition
                    }
                    panX.dispatchRawDelta(diff.x)
                    panY.dispatchRawDelta(diff.y)
                }
            }
            .clip(RectangleShape)
            .drawBehind {
                    drawIntoCanvas { canvas ->
                        canvas.draw(Rect(Offset.Zero, size))
                    }
            })

}

@Composable
fun rememberCanvasState(): CanvasState {
    return remember { CanvasState() }
}

class CanvasState {
    var mousePosition by mutableStateOf(Offset.Zero)
    var size by mutableStateOf(IntSize.Zero)

    var offset by mutableStateOf(Offset.Zero)
    var scale by mutableStateOf(1f)
    
    val virtualPosition by derivedStateOf {
        val relativePosition = mousePosition - offset
        Offset(relativePosition.x / scale, relativePosition.y / scale)
    }

    fun zoom(value: Float, retainPoint: Offset?) {
        val constrained = value.coerceIn(0.1f, 20f)
        if (retainPoint != null && retainPoint.x < size.width && retainPoint.y < size.height) {
            val oldPosition = retainPoint - offset
            val newPosition = oldPosition * constrained / scale
            val delta = newPosition - oldPosition
            offset -= delta
        }
        scale = constrained
    }

    fun zoomTo(x: Int, y: Int, width: Int, height: Int) {
        scale = minOf(
            (size.width) / width.toFloat(),
            (size.height) / height.toFloat()
        ).coerceIn(0.1f, 10f) * 0.8f
        val offsetX = (size.width - width * scale) / 2
        val offsetY = (size.height - height * scale) / 2
        offset = Offset(offsetX - x * scale, offsetY - y * scale)
    }
}
157 Views