diff --git a/showcase-layout-compose/build.gradle.kts b/showcase-layout-compose/build.gradle.kts index baf7efb..733c568 100644 --- a/showcase-layout-compose/build.gradle.kts +++ b/showcase-layout-compose/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig - plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) @@ -19,19 +17,8 @@ kotlin { @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) wasmJs { - outputModuleName = "composeApp" - browser { - commonWebpackConfig { - outputFileName = "composeApp.js" - devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { - static = (static ?: mutableListOf()).apply { - // Serve sources to debug inside browser - add(project.projectDir.path) - } - } - } - } - binaries.executable() + outputModuleName = "showcase-layout-compose" + browser() } @@ -70,7 +57,7 @@ mavenPublishing { publishToMavenCentral() signAllPublications() - coordinates("ly.com.tahaben", "showcase-layout-compose", "1.1.0") + coordinates("ly.com.tahaben", "showcase-layout-compose", "1.1.1") pom { name.set("Showcase Layout Compose") diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt index e46a175..13e0ec9 100644 --- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt +++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalDensity @@ -97,7 +98,9 @@ fun ShowcaseLayout( } val currentContent by rememberUpdatedState(content) val resetDelay by derivedStateOf { animationDuration.toLong() + INDEX_RESET_DELAY } + val layoutCoordinatesState = remember { mutableStateOf(null) } val scope = ShowcaseScopeImpl(greeting) + scope.layoutCoordinatesState = layoutCoordinatesState scope.currentContent() var singleGreetingMsg by remember { mutableStateOf(null) } @@ -133,7 +136,10 @@ fun ShowcaseLayout( } } - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + BoxWithConstraints(modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { layoutCoordinatesState.value = it } + ) { val coroutineScope = rememberCoroutineScope() if (isShowcasing || showCasingItem || isSingleGreeting) { val offset by animateOffsetAsState( @@ -1014,6 +1020,10 @@ fun ShowcaseLayout( class ShowcaseScopeImpl(greeting: ShowcaseMsg?) : ShowcaseScope { private val showcaseDataHashMap = HashMap() + + /** Coordinates of the showcase layout itself, used to convert target positions from + * root space to the layout's local space (the space the overlay canvas draws in). */ + internal var layoutCoordinatesState: State = mutableStateOf(null) override var showcaseEventListener: ShowcaseEventListener? = null private val _showcaseActionFlow = MutableStateFlow(null) val showcaseActionFlow = _showcaseActionFlow.asStateFlow() @@ -1108,11 +1118,22 @@ class ShowcaseScopeImpl(greeting: ShowcaseMsg?) : ShowcaseScope { } fun getPositionFor(index: Int): Offset { - if (index == 0) { - return showcaseDataHashMap[1]?.position ?: Offset(0f, 0f) + val data = showcaseDataHashMap[if (index == 0) 1 else index] ?: return Offset(0f, 0f) + return positionInLayout(data) + } + + /** The stored position is relative to the composition root, but the overlay canvas draws + * in the layout's local space. When the layout doesn't sit at the root origin (status bar + * padding, app bars, ...) the two spaces differ, so convert; the raw root position is only + * a fallback for when either set of coordinates is detached. */ + private fun positionInLayout(data: ShowcaseData): Offset { + val layoutCoordinates = layoutCoordinatesState.value + val targetCoordinates = data.coordinates + return if (layoutCoordinates?.isAttached == true && targetCoordinates?.isAttached == true) { + layoutCoordinates.localPositionOf(targetCoordinates, Offset.Zero) + } else { + data.position } - val p = showcaseDataHashMap[index]?.position ?: Offset(0f, 0f) - return p } fun getHashMapSize(): Int { diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt index 51f0040..e5acd47 100644 --- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt +++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathOperation import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -93,7 +95,9 @@ fun TargetShowcaseLayout( mutableIntStateOf(validatedInitIndex) } val currentContent by rememberUpdatedState(content) + val layoutCoordinatesState = remember { mutableStateOf(null) } val scope = ShowcaseScopeImpl(greeting) + scope.layoutCoordinatesState = layoutCoordinatesState scope.currentContent() val localDensity = LocalDensity.current var singleGreetingMsg by remember { mutableStateOf(null) } @@ -128,7 +132,10 @@ fun TargetShowcaseLayout( } } - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + BoxWithConstraints(modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { layoutCoordinatesState.value = it } + ) { val coroutineScope = rememberCoroutineScope() if (isShowcasing || showCasingItem || isSingleGreeting) { var itemSize = scope.getSizeFor(currentIndex)