JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.24k stars 1.18k forks source link

Composable with offset disappears when scale is below 1f #1559

Closed ryanmitchener closed 11 months ago

ryanmitchener commented 2 years ago

I am creating surface that's able to be panned around and scaled in and out and I ran across this bug. If I keep the scale at 1f, then everything works perfectly fine. However, if I change the scale slightly and pan passed the width of the composables, they disappear. See the attached video for an example

https://user-images.githubusercontent.com/6433145/145070662-83af05cb-5247-454a-8e24-7b6332fdee57.mov

igordmn commented 2 years ago

Could you provide a snippet, where this issue reproduces?

ryanmitchener commented 2 years ago

@igordmn Here is a full snippet of what I have in the video:

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.input.mouse.mouseScrollFilter
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import java.awt.Dimension

fun main() = singleWindowApplication {
    val density = LocalDensity.current
    val minWindowSize = remember(density) { Dimension(1280, 768) }
    if (window.minimumSize != minWindowSize) window.minimumSize = minWindowSize

    CanvasScreen()
}

@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
@Composable
fun CanvasScreen() {
    val density = LocalDensity.current
    val scale = remember { mutableStateOf(1f) }
    val panX = remember { mutableStateOf(0f) }
    val panY = remember { mutableStateOf(0f) }
    val scaleAnim by animateFloatAsState(scale.value, animationSpec = tween(250))

    Column(modifier = Modifier.background(Color(0xFFFFFFFF))) {
        Box(modifier = Modifier.clipToBounds().size(10_000.dp).pointerInput(Unit) {
            while (true) {
                awaitPointerEventScope {
                    awaitFirstDown()

                    while (true) {
                        val event = awaitPointerEvent().changes.first()
                        if (event.changedToUp()) {
                            break
                        } else if (event.positionChanged()) {
                            val posChange = event.positionChange().toDpOffset(density)
                            panX.value += posChange.x.value
                            panY.value += posChange.y.value
                        }
                    }
                }
            }
        }.mouseScrollFilter { event, _ ->
            if (event.delta !is MouseScrollUnit.Line) return@mouseScrollFilter false
            val deltaValue = (event.delta as MouseScrollUnit.Line).value
            scale.value = (scale.value - (deltaValue * .025f)).coerceIn(.1f, 1f)
            false
        }) {
            Test(DpOffset(panX.value.dp, panY.value.dp), scale = scaleAnim, itemOffset = DpOffset(0.dp, 20.dp))
            Test(DpOffset(panX.value.dp, panY.value.dp), itemOffset = DpOffset(400.dp, 20.dp),  scale = scaleAnim)
            Test(DpOffset(panX.value.dp, panY.value.dp), itemOffset = DpOffset(0.dp, 400.dp), scale = scaleAnim)
            Text("Offset: ${panX.value}:${panY.value}, Scale: $scaleAnim")
        }
    }
}

@Composable
fun Test(pan: DpOffset, itemOffset: DpOffset = DpOffset.Zero, scale: Float) {
    val dragOffset = remember(Unit) { mutableStateOf(DpOffset(0.dp, 0.dp)) }
    val density = LocalDensity.current

    Box(
        modifier = Modifier
            .offset(pan)
            .scale(scale)
            .offset(dragOffset.value + itemOffset)
            .pointerInput(Unit) {
                while (true) {
                    awaitPointerEventScope {
                        awaitFirstDown()

                        while (true) {
                            val event = awaitPointerEvent().changes.first()
                            if (event.changedToUp()) {
                                break
                            } else if (event.positionChanged()) {
                                val posChange = event.positionChange().toDpOffset(density)
                                dragOffset.value += posChange
                                event.consumeAllChanges()
                            }
                        }
                    }
                }
            }
            .size(200.dp)
            .background(Color(0xFFBBBBBB)),
        contentAlignment = Alignment.Center
    ) {
        Text("Item Offset:\n${itemOffset}\n\nWidth: 200.dp")
    }
}
igordmn commented 2 years ago

A small reproducer:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication {
    Box(modifier = Modifier.fillMaxSize()) {
        Box(
            modifier = Modifier
                .offset(-100.dp, 0.dp)
                .scale(0.95f)
                .offset(200.dp, 20.dp)
                .size(100.dp)
                .background(Color.LightGray)
        )
        Box(
            modifier = Modifier
                .offset(0.dp, 0.dp)
                .scale(0.95f)
                .offset(200.dp, 20.dp)
                .size(100.dp)
                .background(Color.LightGray)
        )
    }
}

(Isn't reproducible on Android)

igordmn commented 11 months ago

The issue is the same as in https://github.com/JetBrains/compose-multiplatform/issues/2807. Let's keep 2807, as the core issue is more visible there.

okushnikov commented 2 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.