diff --git a/appyx-components/experimental/cards/common/build.gradle.kts b/appyx-components/experimental/cards/common/build.gradle.kts index 054ebae60..dc1a08eb0 100644 --- a/appyx-components/experimental/cards/common/build.gradle.kts +++ b/appyx-components/experimental/cards/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -24,6 +26,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-components-experimental-cards-commons" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-components-experimental-cards-commons-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() iosArm64() @@ -56,11 +74,16 @@ kotlin { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) add("kspIosArm64", project(":ksp:appyx-processor")) add("kspIosX64", project(":ksp:appyx-processor")) add("kspIosSimulatorArm64", project(":ksp:appyx-processor")) diff --git a/appyx-components/experimental/cards/common/karma.config.d/wasm/config.js b/appyx-components/experimental/cards/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-components/experimental/cards/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-components/experimental/promoter/common/build.gradle.kts b/appyx-components/experimental/promoter/common/build.gradle.kts index 10f2e7458..54ffbbef3 100644 --- a/appyx-components/experimental/promoter/common/build.gradle.kts +++ b/appyx-components/experimental/promoter/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -24,6 +26,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-components-experimental-promoter-commons" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-components-experimental-promoter-commons-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() iosArm64() @@ -57,11 +75,16 @@ kotlin { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) add("kspIosArm64", project(":ksp:appyx-processor")) add("kspIosX64", project(":ksp:appyx-processor")) add("kspIosSimulatorArm64", project(":ksp:appyx-processor")) diff --git a/appyx-components/experimental/promoter/common/karma.config.d/wasm/config.js b/appyx-components/experimental/promoter/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-components/experimental/promoter/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-components/experimental/puzzle15/common/build.gradle.kts b/appyx-components/experimental/puzzle15/common/build.gradle.kts index 6b484b3bc..6a8a08133 100644 --- a/appyx-components/experimental/puzzle15/common/build.gradle.kts +++ b/appyx-components/experimental/puzzle15/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -24,6 +26,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-components-experimental-puzzle15-commons" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-components-experimental-puzzle15-commons-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } sourceSets { val commonMain by getting { @@ -44,9 +62,14 @@ kotlin { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/appyx-components/experimental/puzzle15/common/karma.config.d/wasm/config.js b/appyx-components/experimental/puzzle15/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-components/experimental/puzzle15/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-components/experimental/puzzle15/web/build.gradle.kts b/appyx-components/experimental/puzzle15/web/build.gradle.kts index 332d30812..ef97d2bbe 100644 --- a/appyx-components/experimental/puzzle15/web/build.gradle.kts +++ b/appyx-components/experimental/puzzle15/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -9,6 +11,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-components-experimental-puzzle15-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { diff --git a/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js b/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/experimental/puzzle15/web/main.js.kt b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/experimental/puzzle15/web/main.js.kt new file mode 100644 index 000000000..4adc93d61 --- /dev/null +++ b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/experimental/puzzle15/web/main.js.kt @@ -0,0 +1,51 @@ +package com.bumble.appyx.experimental.puzzle15.web + +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.components.experimental.puzzle15.ui.Puzzle15Ui + +external fun onWasmReady(onReady: () -> Unit) + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + onWasmReady { + CanvasBasedWindow("Puzzle15") { + val requester = remember { FocusRequester() } + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + Puzzle15Ui( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .focusRequester(requester) + .focusable(), + ) + } + + LaunchedEffect(Unit) { + requester.requestFocus() + } + } + } +} diff --git a/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..5d3cb39cc --- /dev/null +++ b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Puzzle15 + + + + +
+ +
+ + + diff --git a/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..8655f2e76 --- /dev/null +++ b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,12 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} diff --git a/appyx-components/internal/test-drive/common/build.gradle.kts b/appyx-components/internal/test-drive/common/build.gradle.kts index 926510668..6be5ae252 100644 --- a/appyx-components/internal/test-drive/common/build.gradle.kts +++ b/appyx-components/internal/test-drive/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -23,6 +25,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-components-internal-testdrive-common" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-components-internal-testdrive-common-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } sourceSets { val commonMain by getting { @@ -44,9 +62,14 @@ kotlin { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/appyx-components/internal/test-drive/common/karma.config.d/wasm/config.js b/appyx-components/internal/test-drive/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-components/internal/test-drive/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-components/standard/backstack/common/build.gradle.kts b/appyx-components/standard/backstack/common/build.gradle.kts index ae4cd61cd..195cc2bb2 100644 --- a/appyx-components/standard/backstack/common/build.gradle.kts +++ b/appyx-components/standard/backstack/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -24,6 +26,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-components-stable-backstack-commons" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-components-stable-backstack-commons-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() @@ -47,6 +65,7 @@ kotlin { val androidMain by getting val desktopMain by getting val jsMain by getting + val wasmJsMain by getting val iosX64Main by getting val iosArm64Main by getting @@ -60,11 +79,16 @@ kotlin { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) add("kspIosArm64", project(":ksp:appyx-processor")) add("kspIosX64", project(":ksp:appyx-processor")) add("kspIosSimulatorArm64", project(":ksp:appyx-processor")) diff --git a/appyx-components/standard/backstack/common/karma.config.d/wasm/config.js b/appyx-components/standard/backstack/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-components/standard/backstack/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-components/standard/spotlight/common/build.gradle.kts b/appyx-components/standard/spotlight/common/build.gradle.kts index 2b56f8952..5942cad0e 100644 --- a/appyx-components/standard/spotlight/common/build.gradle.kts +++ b/appyx-components/standard/spotlight/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -24,6 +26,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-components-stable-spotlight-commons" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-components-stable-spotlight-commons-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() @@ -63,11 +81,16 @@ kotlin { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) add("kspIosArm64", project(":ksp:appyx-processor")) add("kspIosX64", project(":ksp:appyx-processor")) add("kspIosSimulatorArm64", project(":ksp:appyx-processor")) diff --git a/appyx-components/standard/spotlight/common/karma.config.d/wasm/config.js b/appyx-components/standard/spotlight/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-components/standard/spotlight/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-interactions/common/build.gradle.kts b/appyx-interactions/common/build.gradle.kts index 72ee1489e..effc21525 100644 --- a/appyx-interactions/common/build.gradle.kts +++ b/appyx-interactions/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") kotlin("plugin.serialization") @@ -24,6 +26,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-interactions-common" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-interactions-common-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() @@ -79,3 +97,7 @@ kotlin { } } } + +compose.experimental { + web.application {} +} diff --git a/appyx-interactions/common/karma.config.d/wasm/config.js b/appyx-interactions/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-interactions/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt b/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt index b66bfb4e3..ba9d2071b 100644 --- a/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt +++ b/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt @@ -1,3 +1,3 @@ package com.bumble.appyx.interactions -actual fun getPlatformName(): String = "Web" +actual fun getPlatformName(): String = "Kotlin/JS" diff --git a/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/SystemClock.kt b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/SystemClock.kt new file mode 100644 index 000000000..90b265262 --- /dev/null +++ b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/SystemClock.kt @@ -0,0 +1,13 @@ +package com.bumble.appyx.interactions + +import kotlinx.browser.window +import kotlin.math.roundToLong + +actual object SystemClock { + + actual fun nanoTime(): Long = + (window.performance.now() * MillisToNanos).roundToLong() + + private const val MillisToNanos = 1_000_000L + +} diff --git a/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/UUID.kt b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/UUID.kt new file mode 100644 index 000000000..e3ae0ca8b --- /dev/null +++ b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/UUID.kt @@ -0,0 +1,13 @@ +package com.bumble.appyx.interactions + +@JsModule("uuid") +private external object Uuid { + fun v4(): String +} + +actual object UUID { + + actual fun randomUUID(): String = + Uuid.v4() + +} diff --git a/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/platform.kt b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/platform.kt new file mode 100644 index 000000000..a155aefee --- /dev/null +++ b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/platform.kt @@ -0,0 +1,3 @@ +package com.bumble.appyx.interactions + +actual fun getPlatformName(): String = "Kotlin/Wasm" diff --git a/appyx-navigation/common/build.gradle.kts b/appyx-navigation/common/build.gradle.kts index 11c1cc48c..4b5df9db5 100644 --- a/appyx-navigation/common/build.gradle.kts +++ b/appyx-navigation/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -9,6 +11,7 @@ appyx { androidNamespace.set("com.bumble.appyx.navigation") } +@OptIn(ExperimentalWasmDsl::class) kotlin { androidTarget { publishLibraryVariants("release") @@ -22,6 +25,21 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-navigation-common" browser() + binaries.executable() + } + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-navigation-common-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() @@ -98,3 +116,7 @@ android { androidTestImplementation(project(":utils:testing-ui")) } } + +compose.experimental { + web.application {} +} diff --git a/appyx-navigation/common/karma.config.d/wasm/config.js b/appyx-navigation/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/appyx-navigation/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/appyx-navigation/common/src/jsMain/kotlin/com/bumble/appyx/navigation/integration/BrowserViewportWindow.kt b/appyx-navigation/common/src/jsMain/kotlin/com/bumble/appyx/navigation/integration/BrowserViewportWindow.kt deleted file mode 100644 index 90f94b819..000000000 --- a/appyx-navigation/common/src/jsMain/kotlin/com/bumble/appyx/navigation/integration/BrowserViewportWindow.kt +++ /dev/null @@ -1,82 +0,0 @@ -// From Slack by OliverO -// See: https://kotlinlang.slack.com/archives/C01F2HV7868/p1660083429206369?thread_ts=1660083398.571449&cid=C01F2HV7868 -// adapted with scaling fix from https://github.com/OliverO2/compose-counting-grid/blob/master/src/frontendJsMain/kotlin/BrowserViewportWindow.kt - -@file:Suppress( - "INVISIBLE_MEMBER", - "INVISIBLE_REFERENCE", - "EXPOSED_PARAMETER_TYPE" -) // WORKAROUND: ComposeWindow and ComposeLayer are internal - -package com.bumble.appyx.navigation.integration - -import androidx.compose.runtime.Composable -import androidx.compose.ui.window.ComposeWindow -import kotlinx.browser.document -import kotlinx.browser.window -import org.w3c.dom.HTMLCanvasElement -import org.w3c.dom.HTMLStyleElement -import org.w3c.dom.HTMLTitleElement - -private const val CANVAS_ELEMENT_ID = "ComposeTarget" // Hardwired into ComposeWindow - -/** - * A Skiko/Canvas-based top-level window using the browser's entire viewport. Supports resizing. - */ -@Composable -@Suppress("FunctionNaming") -fun BrowserViewportWindow( - title: String = "Untitled", - content: @Composable () -> Unit -) { - val htmlHeadElement = document.head!! - htmlHeadElement.appendChild( - (document.createElement("style") as HTMLStyleElement).apply { - type = "text/css" - appendChild( - document.createTextNode( - """ - html, body { - overflow: hidden; - margin: 0 !important; - padding: 0 !important; - } - - #$CANVAS_ELEMENT_ID { - outline: none; - } - """.trimIndent() - ) - ) - } - ) - - fun HTMLCanvasElement.fillViewportSize() { - setAttribute("width", "${window.innerWidth}") - setAttribute("height", "${window.innerHeight}") - } - - val canvas = (document.getElementById(CANVAS_ELEMENT_ID) as HTMLCanvasElement).apply { - fillViewportSize() - } - - ComposeWindow(canvasId = "Appyx", content = content).apply { - window.addEventListener("resize", { - canvas.fillViewportSize() - layer.layer.attachTo(canvas) - layer.layer.needRedraw() - val scale = layer.layer.contentScale - layer.setSize( - (canvas.width / scale * density.density).toInt(), - (canvas.height / scale * density.density).toInt() - ) - }) - - // WORKAROUND: ComposeWindow does not implement `setTitle(title)` - val htmlTitleElement = ( - htmlHeadElement.getElementsByTagName("title").item(0) - ?: document.createElement("title").also { htmlHeadElement.appendChild(it) } - ) as HTMLTitleElement - htmlTitleElement.textContent = title - } -} diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/MainIntegrationPoint.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/MainIntegrationPoint.kt new file mode 100644 index 000000000..2adb723bb --- /dev/null +++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/MainIntegrationPoint.kt @@ -0,0 +1,16 @@ +package com.bumble.appyx.navigation.integration + + +class MainIntegrationPoint : IntegrationPoint() { + override val isChangingConfigurations: Boolean + get() = false + + @Suppress("EmptyFunctionBlock") + override fun onRootFinished() { + } + + @Suppress("EmptyFunctionBlock") + override fun handleUpNavigation() { + + } +} diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/WebNodeHost.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/WebNodeHost.kt new file mode 100644 index 000000000..b2b2b30d4 --- /dev/null +++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/WebNodeHost.kt @@ -0,0 +1,65 @@ +package com.bumble.appyx.navigation.integration + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.navigation.node.Node +import com.bumble.appyx.navigation.platform.LocalOnBackPressedDispatcherOwner +import com.bumble.appyx.navigation.platform.OnBackPressedDispatcher +import com.bumble.appyx.navigation.platform.OnBackPressedDispatcherOwner +import com.bumble.appyx.navigation.platform.PlatformLifecycleRegistry +import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory +import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +/** + * Composable function to host [Node<*>]. + * + * This convenience wrapper provides an [OnBackPressedDispatcherOwner] hooked up to the + * [.onBackPressedEvents] flow to simplify implementing the global "go back" functionality + * that is a common concept in the Appyx framework. + */ +@Suppress("ComposableParamOrder") +@Composable +fun > WebNodeHost( + screenSize: ScreenSize, + onBackPressedEvents: Flow, + modifier: Modifier = Modifier, + integrationPoint: IntegrationPoint = remember { MainIntegrationPoint() }, + customisations: NodeCustomisationDirectory = remember { NodeCustomisationDirectoryImpl() }, + factory: NodeFactory +) { + val platformLifecycleRegistry = remember { + PlatformLifecycleRegistry() + } + val onBackPressedDispatcherOwner = remember { + object : OnBackPressedDispatcherOwner { + override val onBackPressedDispatcher: OnBackPressedDispatcher = + OnBackPressedDispatcher { integrationPoint.handleUpNavigation() } + } + } + + val scope = rememberCoroutineScope() + LaunchedEffect(onBackPressedEvents) { + scope.launch { + onBackPressedEvents.collect { + onBackPressedDispatcherOwner.onBackPressedDispatcher.onBackPressed() + } + } + } + + CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides onBackPressedDispatcherOwner) { + NodeHost( + lifecycle = platformLifecycleRegistry, + integrationPoint = integrationPoint, + modifier = modifier, + customisations = customisations, + screenSize = screenSize, + factory = factory, + ) + } +} diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedCallback.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedCallback.kt new file mode 100644 index 000000000..a568631a5 --- /dev/null +++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedCallback.kt @@ -0,0 +1,59 @@ +package com.bumble.appyx.navigation.platform + +interface Cancellable { + /** + * Cancel the subscription. This call should be idempotent, making it safe to + * call multiple times. + */ + fun cancel() +} + +/** + * Create a [OnBackPressedCallback]. + * + * @param isEnabled The default enabled state for this callback. + * @see .setEnabled + */ +abstract class OnBackPressedCallback( + /** + * Set the enabled state of the callback. Only when this callback + * is enabled will it receive callbacks to [.handleOnBackPressed]. + * + * @param isEnabled whether the callback should be considered enabled + */ + var isEnabled: Boolean +) { + /** + * Checks whether this callback should be considered enabled. Only when this callback + * is enabled will it receive callbacks to [.handleOnBackPressed]. + * + * @return Whether this callback should be considered enabled. + */ + private val cancellables: MutableList = mutableListOf() + + /** + * Removes this callback from any [OnBackPressedDispatcher] it is currently + * added to. + */ + fun remove() { + for (cancellable in cancellables) { + cancellable.cancel() + } + } + + /** + * Callback for handling the [OnBackPressedDispatcher.onBackPressed] event. + */ + abstract fun handleOnBackPressed() + fun addCancellable(cancellable: Cancellable) { + run { + cancellables.add(cancellable) + } + } + + fun removeCancellable(cancellable: Cancellable) { + run { + cancellables.remove(cancellable) + } + } +} diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedDispatcher.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedDispatcher.kt new file mode 100644 index 000000000..0bf083749 --- /dev/null +++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedDispatcher.kt @@ -0,0 +1,69 @@ +package com.bumble.appyx.navigation.platform + +import androidx.compose.ui.util.fastForEachReversed + +/** + * Adapted from Android's OnBackPressedDispatcher. + * + * Create a new OnBackPressedDispatcher that dispatches [.onBackPressed] events + * to one or more [OnBackPressedCallback] instances. + * + * @param fallbackOnBackPressed The Runnable that should be triggered if + * [.onBackPressed] is called when no [OnBackPressedCallback] have been registered. + */ +class OnBackPressedDispatcher(private val fallbackOnBackPressed: (() -> Unit)? = null) { + val onBackPressedCallbacks: ArrayDeque = + ArrayDeque() + + /** + * Internal implementation of [.addCallback] that gives + * access to the [Cancellable] that specifically removes this callback from + * the dispatcher without relying on [OnBackPressedCallback.remove] which + * is what external developers should be using. + * + * @param onBackPressedCallback The callback to add + * @return a [Cancellable] which can be used to [cancel][Cancellable.cancel] + * the callback and remove it from the set of OnBackPressedCallbacks. + */ + fun addCancellableCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable { + onBackPressedCallbacks.add(onBackPressedCallback) + val cancellable = OnBackPressedCancellable(onBackPressedCallback) + onBackPressedCallback.addCancellable(cancellable) + return cancellable + } + + /** + * Trigger a call to the currently added [callbacks][OnBackPressedCallback] in reverse + * order in which they were added. Only if the most recently added callback is not + * [enabled][OnBackPressedCallback.isEnabled] + * will any previously added callback be called. + * + * + * If [.hasEnabledCallbacks] is `false` when this method is called, the + * fallback Runnable set by [the constructor][.OnBackPressedDispatcher] + * will be triggered. + */ + fun onBackPressed() { + onBackPressedCallbacks.fastForEachReversed { callback -> + if (callback.isEnabled) { + callback.handleOnBackPressed() + return + } + } + fallbackOnBackPressed?.invoke() + } + + private inner class OnBackPressedCancellable(onBackPressedCallback: OnBackPressedCallback) : + Cancellable { + private val onBackPressedCallback: OnBackPressedCallback + + init { + this.onBackPressedCallback = onBackPressedCallback + } + + override fun cancel() { + onBackPressedCallbacks.remove(onBackPressedCallback) + onBackPressedCallback.removeCancellable(this) + } + } +} diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformBackHandler.wasmJs.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformBackHandler.wasmJs.kt new file mode 100644 index 000000000..46113be27 --- /dev/null +++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformBackHandler.wasmJs.kt @@ -0,0 +1,56 @@ +package com.bumble.appyx.navigation.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState + +@Suppress("CompositionLocalAllowlist") +val LocalOnBackPressedDispatcherOwner: ProvidableCompositionLocal = + compositionLocalOf { + object : OnBackPressedDispatcherOwner { + override val onBackPressedDispatcher: OnBackPressedDispatcher + get() = OnBackPressedDispatcher(null) + } + } + +interface OnBackPressedDispatcherOwner { + val onBackPressedDispatcher: OnBackPressedDispatcher +} + +@Composable +actual fun PlatformBackHandler( + enabled: Boolean, + onBack: () -> Unit +) { + // Safely update the current `onBack` lambda when a new one is provided + val currentOnBack by rememberUpdatedState(onBack) + // Remember in Composition a back callback that calls the `onBack` lambda + val backCallback = remember { + object : OnBackPressedCallback(enabled) { + override fun handleOnBackPressed() { + currentOnBack() + } + } + } + // On every successful composition, update the callback with the `enabled` value + SideEffect { + backCallback.isEnabled = enabled + } + + // register for back events only whilst present in the composition + val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) { + "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner" + }.onBackPressedDispatcher + DisposableEffect(backDispatcher) { + val cancellable = backDispatcher.addCancellableCallback(backCallback) + + onDispose { + cancellable.cancel() + } + } +} diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformLifecycleRegistry.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformLifecycleRegistry.kt new file mode 100644 index 000000000..518ca5afa --- /dev/null +++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformLifecycleRegistry.kt @@ -0,0 +1,91 @@ +package com.bumble.appyx.navigation.platform + +import com.bumble.appyx.navigation.lifecycle.CommonLifecycleOwner +import com.bumble.appyx.navigation.lifecycle.DefaultPlatformLifecycleObserver +import com.bumble.appyx.navigation.lifecycle.Lifecycle +import com.bumble.appyx.navigation.lifecycle.PlatformLifecycleEventObserver +import com.bumble.appyx.navigation.lifecycle.PlatformLifecycleObserver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive + +actual class PlatformLifecycleRegistry : Lifecycle { + + private val managedDefaultLifecycleObservers: MutableList = + ArrayList() + private val managedLifecycleEventObservers: MutableList = + ArrayList() + + private var _currentState: Lifecycle.State = Lifecycle.State.INITIALIZED + override var currentState: Lifecycle.State + get() = _currentState + set(value) { + when (value) { + Lifecycle.State.INITIALIZED -> Unit + Lifecycle.State.CREATED -> { + managedDefaultLifecycleObservers.forEach { it.onCreate() } + managedLifecycleEventObservers.forEach { + it.onStateChanged( + value, + Lifecycle.Event.ON_CREATE + ) + } + } + Lifecycle.State.STARTED -> { + managedDefaultLifecycleObservers.forEach { it.onStart() } + managedLifecycleEventObservers.forEach { + it.onStateChanged( + value, + Lifecycle.Event.ON_START + ) + } + } + Lifecycle.State.RESUMED -> { + managedDefaultLifecycleObservers.forEach { it.onResume() } + managedLifecycleEventObservers.forEach { + it.onStateChanged( + value, + Lifecycle.Event.ON_RESUME + ) + } + } + Lifecycle.State.DESTROYED -> { + managedDefaultLifecycleObservers.forEach { it.onDestroy() } + managedLifecycleEventObservers.forEach { + it.onStateChanged( + value, + Lifecycle.Event.ON_DESTROY + ) + } + if (coroutineScope.isActive) coroutineScope.cancel("lifecycle was destroyed") + } + } + _currentState = value + } + + override val coroutineScope: CoroutineScope by lazy { MainScope() } + + override fun addObserver(observer: PlatformLifecycleObserver) { + when (observer) { + is DefaultPlatformLifecycleObserver -> managedDefaultLifecycleObservers.add(observer) + is PlatformLifecycleEventObserver -> managedLifecycleEventObservers.add(observer) + } + } + + override fun removeObserver(observer: PlatformLifecycleObserver) { + when (observer) { + is DefaultPlatformLifecycleObserver -> managedDefaultLifecycleObservers.remove(observer) + is PlatformLifecycleEventObserver -> managedLifecycleEventObservers.remove(observer) + } + } + + actual fun setCurrentState(state: Lifecycle.State) { + currentState = state + } + + actual companion object { + actual fun create(owner: CommonLifecycleOwner): PlatformLifecycleRegistry = + PlatformLifecycleRegistry() + } +} diff --git a/demos/appyx-interactions/web/build.gradle.kts b/demos/appyx-interactions/web/build.gradle.kts index ba3d15648..b0036472e 100644 --- a/demos/appyx-interactions/web/build.gradle.kts +++ b/demos/appyx-interactions/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -9,6 +11,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-interactions-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { diff --git a/demos/appyx-interactions/web/karma.config.d/wasm/config.js b/demos/appyx-interactions/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/appyx-interactions/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/appyx-interactions/web/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/main.js.kt b/demos/appyx-interactions/web/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/main.js.kt new file mode 100644 index 000000000..34426f6f8 --- /dev/null +++ b/demos/appyx-interactions/web/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/main.js.kt @@ -0,0 +1,79 @@ +package com.bumble.appyx.interactions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.components.internal.testdrive.TestDriveExperiment +import com.bumble.appyx.components.internal.testdrive.ui.md_amber_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_blue_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_blue_grey_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_cyan_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_grey_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_indigo_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_light_blue_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_light_green_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_lime_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_pink_500 +import com.bumble.appyx.components.internal.testdrive.ui.md_teal_500 + +external fun onWasmReady(onReady: () -> Unit) + +val manatee = Color(0xFF8D99AE) +val silver_sand = Color(0xFFBDC6D1) +val sizzling_red = Color(0xFFF05D5E) +val atomic_tangerine = Color(0xFFF0965D) + +val colors = listOf( + manatee, + sizzling_red, + atomic_tangerine, + silver_sand, + md_pink_500, + md_indigo_500, + md_blue_500, + md_light_blue_500, + md_cyan_500, + md_teal_500, + md_light_green_500, + md_lime_500, + md_amber_500, + md_grey_500, + md_blue_grey_500 +) + +enum class InteractionTarget { + Child1 +} + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + onWasmReady { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + TestDriveExperiment( + screenWidthPx = size.width, + screenHeightPx = size.height, + element = InteractionTarget.Child1, + modifier = Modifier.fillMaxSize().background(Color.Black), + ) + } + } + } + } +} diff --git a/demos/appyx-interactions/web/src/wasmJsMain/resources/index.html b/demos/appyx-interactions/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..834b74027 --- /dev/null +++ b/demos/appyx-interactions/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/appyx-interactions/web/src/wasmJsMain/resources/styles.css b/demos/appyx-interactions/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..8655f2e76 --- /dev/null +++ b/demos/appyx-interactions/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,12 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} diff --git a/demos/appyx-navigation/common/build.gradle.kts b/demos/appyx-navigation/common/build.gradle.kts index 85c316443..9e77e0a02 100644 --- a/demos/appyx-navigation/common/build.gradle.kts +++ b/demos/appyx-navigation/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -23,6 +25,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "demo-appyx-navigation-common" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "demo-appyx-navigation-common-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() iosArm64() @@ -90,11 +108,16 @@ android { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) add("kspIosArm64", project(":ksp:appyx-processor")) add("kspIosX64", project(":ksp:appyx-processor")) add("kspIosSimulatorArm64", project(":ksp:appyx-processor")) diff --git a/demos/appyx-navigation/common/karma.config.d/wasm/config.js b/demos/appyx-navigation/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/appyx-navigation/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/platform/PlatformName.kt b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/platform/PlatformName.kt new file mode 100644 index 000000000..ff4c7405d --- /dev/null +++ b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/platform/PlatformName.kt @@ -0,0 +1,3 @@ +package com.bumble.appyx.demos.navigation.platform + +actual fun getPlatformName(): String = "Kotlin/Wasm" diff --git a/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/ui/EmbeddableResourceImage.kt b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/ui/EmbeddableResourceImage.kt new file mode 100644 index 000000000..32c3edfc2 --- /dev/null +++ b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/ui/EmbeddableResourceImage.kt @@ -0,0 +1,24 @@ +package com.bumble.appyx.demos.navigation.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import com.bumble.appyx.imageloader.ResourceImage + +private const val EMBED_URL_PATH = "appyx/samples/documentation-appyx-navigation/" + +@Composable +actual fun EmbeddableResourceImage( + path: String, + contentDescription: String, + contentScale: ContentScale, + modifier: Modifier +) { + ResourceImage( + path = EMBED_URL_PATH + path, + fallbackUrl = path, + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier, + ) +} diff --git a/demos/appyx-navigation/web/build.gradle.kts b/demos/appyx-navigation/web/build.gradle.kts index d4fa3ec95..668b1c58c 100644 --- a/demos/appyx-navigation/web/build.gradle.kts +++ b/demos/appyx-navigation/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-navigation-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,22 +52,41 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } -tasks.register("copyResources") { +tasks.register("jsCopyResources") { // Dirs containing files we want to copy from("../common/src/commonMain/resources") // Output for web resources - into("$buildDir/processedResources/js/main") + into("${layout.buildDirectory}/processedResources/js/main") include("**/*") } tasks.named("jsBrowserProductionExecutableDistributeResources") { - dependsOn("copyResources") + dependsOn("jsCopyResources") } tasks.named("jsMainClasses") { - dependsOn("copyResources") + dependsOn("jsCopyResources") +} + +tasks.register("wasmJsCopyResources") { + // Dirs containing files we want to copy + from("../common/src/commonMain/resources") + + // Output for web resources + into("${layout.buildDirectory}/processedResources/wasmJs/main") + + include("**/*") +} + +tasks.named("wasmJsBrowserProductionExecutableDistributeResources") { + dependsOn("wasmJsCopyResources") } + +tasks.named("wasmJsMainClasses") { + dependsOn("wasmJsCopyResources") +} \ No newline at end of file diff --git a/demos/appyx-navigation/web/karma.config.d/wasm/config.js b/demos/appyx-navigation/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/appyx-navigation/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/Main.kt b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/Main.kt new file mode 100644 index 000000000..e43f5ab47 --- /dev/null +++ b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/Main.kt @@ -0,0 +1,138 @@ +package com.bumble.appyx.navigation + +import androidx.compose.foundation.border +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_primary +import com.bumble.appyx.demos.navigation.navigator.LocalNavigator +import com.bumble.appyx.demos.navigation.navigator.Navigator +import com.bumble.appyx.demos.navigation.node.root.RootNode +import com.bumble.appyx.demos.navigation.ui.AppyxSampleAppTheme +import com.bumble.appyx.navigation.integration.ScreenSize +import com.bumble.appyx.navigation.integration.WebNodeHost +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + val events: Channel = Channel() + val navigator = Navigator() + appyxSample { + CanvasBasedWindow("Appyx navigation demo") { + CakeApp(events, navigator) + } + } +} + +private val containerShape = RoundedCornerShape(8) + +@Composable +private fun CakeApp(events: Channel, navigator: Navigator) { + AppyxSampleAppTheme(darkTheme = true, themeTypography = webTypography) { + val requester = remember { FocusRequester() } + var hasFocus by remember { mutableStateOf(false) } + + var screenSize by remember { mutableStateOf(ScreenSize(0.dp, 0.dp)) } + val eventScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) } + + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { screenSize = ScreenSize(it.width.dp, it.height.dp) } + .onKeyEvent { + onKeyEvent(it, events, eventScope) + } + .focusRequester(requester) + .focusable() + .onFocusChanged { hasFocus = it.hasFocus }, + color = MaterialTheme.colorScheme.background, + ) { + CompositionLocalProvider(LocalNavigator provides navigator) { + BlackContainer { + WebNodeHost( + screenSize = screenSize, + onBackPressedEvents = events.receiveAsFlow(), + ) { nodeContext -> + RootNode( + nodeContext = nodeContext, + plugins = listOf(navigator) + ) + } + } + } + } + + if (!hasFocus) { + LaunchedEffect(Unit) { + requester.requestFocus() + } + } + } +} + +@Composable +private fun BlackContainer(content: @Composable () -> Unit) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .aspectRatio(0.56f) + .border(4.dp, color_primary, containerShape) + .clip(containerShape) + ) { + content() + } + } +} + +private fun onKeyEvent( + keyEvent: KeyEvent, + events: Channel, + coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main), +): Boolean = + when { + keyEvent.type == KeyEventType.KeyUp && keyEvent.key == Key.Backspace -> { + coroutineScope.launch { events.send(Unit) } + true + } + + else -> false + } diff --git a/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/WebTypography.kt b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/WebTypography.kt new file mode 100644 index 000000000..833b55cac --- /dev/null +++ b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/WebTypography.kt @@ -0,0 +1,44 @@ +package com.bumble.appyx.navigation + +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.sp +import com.bumble.appyx.demos.navigation.ui.typography + +internal val webTypography = typography.copy( + bodySmall = typography.bodySmall.copy( + fontSize = 8.sp, + fontFamily = FontFamily.SansSerif, + ), + bodyMedium = typography.bodyMedium.copy( + fontSize = 10.sp, + fontFamily = FontFamily.SansSerif, + ), + bodyLarge = typography.bodyLarge.copy( + fontSize = 12.sp, + fontFamily = FontFamily.SansSerif, + ), + titleSmall = typography.titleSmall.copy( + fontSize = 8.sp, + fontFamily = FontFamily.SansSerif, + ), + titleMedium = typography.titleMedium.copy( + fontSize = 10.sp, + fontFamily = FontFamily.SansSerif, + ), + titleLarge = typography.titleLarge.copy( + fontSize = 12.sp, + fontFamily = FontFamily.SansSerif, + ), + headlineSmall = typography.headlineSmall.copy( + fontSize = 14.sp, + fontFamily = FontFamily.SansSerif, + ), + headlineMedium = typography.headlineMedium.copy( + fontSize = 16.sp, + fontFamily = FontFamily.SansSerif, + ), + headlineLarge = typography.headlineLarge.copy( + fontSize = 18.sp, + fontFamily = FontFamily.SansSerif, + ), +) diff --git a/demos/appyx-navigation/web/src/wasmJsMain/resources/index.html b/demos/appyx-navigation/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..07c3ca9bc --- /dev/null +++ b/demos/appyx-navigation/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Navigation Demo + + + + +
+ +
+ + + diff --git a/demos/appyx-navigation/web/src/wasmJsMain/resources/styles.css b/demos/appyx-navigation/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..8655f2e76 --- /dev/null +++ b/demos/appyx-navigation/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,12 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} diff --git a/demos/common/build.gradle.kts b/demos/common/build.gradle.kts index abcbffc86..2d6862b5f 100644 --- a/demos/common/build.gradle.kts +++ b/demos/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -22,6 +24,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-demos-commons" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-demos-commons-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() iosArm64() @@ -61,3 +79,7 @@ kotlin { } } } + +compose.experimental { + web.application {} +} diff --git a/demos/common/karma.config.d/wasm/config.js b/demos/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/image-loader/common/build.gradle.kts b/demos/image-loader/common/build.gradle.kts index b8d70eb27..81da378f0 100644 --- a/demos/image-loader/common/build.gradle.kts +++ b/demos/image-loader/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -21,6 +23,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-navigation-imageloader" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-navigation-imageloader-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() @@ -87,3 +105,7 @@ android { androidTestImplementation(composeBom) } } + +compose.experimental { + web.application {} +} diff --git a/demos/image-loader/common/karma.config.d/wasm/config.js b/demos/image-loader/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/image-loader/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/image-loader/common/src/wasmJsMain/kotlin/com/bumble/appyx/imageloader/ImageBitmap.wasmJs.kt b/demos/image-loader/common/src/wasmJsMain/kotlin/com/bumble/appyx/imageloader/ImageBitmap.wasmJs.kt new file mode 100644 index 000000000..b614c08dd --- /dev/null +++ b/demos/image-loader/common/src/wasmJsMain/kotlin/com/bumble/appyx/imageloader/ImageBitmap.wasmJs.kt @@ -0,0 +1,9 @@ +package com.bumble.appyx.imageloader + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap + +actual fun ByteArray.toImageBitmap(): ImageBitmap { + // TODO This doesn't work. Fix this for web + return org.jetbrains.skia.Image.makeFromEncoded(this).toComposeImageBitmap() +} diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts index 82e3a6a71..be29c162b 100644 --- a/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-backstack-fader-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -26,6 +42,11 @@ kotlin { implementation(project(":demos:mkdocs:common")) } } + val wasmJsMain by getting { + dependencies { + implementation(project(":demos:mkdocs:common")) + } + } } } @@ -36,4 +57,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/fader/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/fader/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/BackStackFaderSample.kt b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/BackStackFaderSample.kt new file mode 100644 index 000000000..9dec56871 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/BackStackFaderSample.kt @@ -0,0 +1,51 @@ +package com.bumble.appyx.demos.backstack.fader + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.backstack.BackStack +import com.bumble.appyx.components.backstack.BackStackModel +import com.bumble.appyx.components.backstack.operation.pop +import com.bumble.appyx.components.backstack.operation.push +import com.bumble.appyx.components.backstack.ui.fader.BackStackFader +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.ChildSize +import com.bumble.appyx.demos.common.InteractionTarget +import com.bumble.appyx.interactions.gesture.GestureFactory + +@Composable +fun BackStackFaderSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + BackStackModel( + initialTarget = InteractionTarget.Element(), + savedStateMap = null + ) + } + val backStack = BackStack( + scope = coroutineScope, + model = model, + visualisation = { BackStackFader(it) }, + gestureFactory = { GestureFactory.Noop() }, + animationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2), + ) + val actions = mapOf( + "Pop" to { backStack.pop() }, + "Push" to { backStack.push(InteractionTarget.Element()) } + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = backStack, + actions = actions, + childSize = ChildSize.MAX, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/main.js.kt b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/main.js.kt new file mode 100644 index 000000000..15e0c3fe8 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.backstack.fader + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + BackStackFaderSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts index 5f407941d..412125160 100644 --- a/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-backstack-parallax-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/parallax/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/parallax/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/BackStackParallaxSample.kt b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/BackStackParallaxSample.kt new file mode 100644 index 000000000..16b53dc53 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/BackStackParallaxSample.kt @@ -0,0 +1,51 @@ +package com.bumble.appyx.demos.backstack.parallax + +import androidx.compose.animation.core.Spring.StiffnessVeryLow +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.backstack.BackStack +import com.bumble.appyx.components.backstack.BackStackModel +import com.bumble.appyx.components.backstack.operation.pop +import com.bumble.appyx.components.backstack.operation.push +import com.bumble.appyx.components.backstack.ui.parallax.BackStackParallax +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.ChildSize +import com.bumble.appyx.demos.common.InteractionTarget + +@Composable +fun BackStackParallaxSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + BackStackModel( + initialTargets = List(5) { InteractionTarget.Element() }, + savedStateMap = null, + ) + } + val backStack = + BackStack( + scope = coroutineScope, + model = model, + visualisation = { BackStackParallax(it) }, + gestureFactory = { BackStackParallax.Gestures(it) }, + animationSpec = spring(stiffness = StiffnessVeryLow), + ) + val actions = mapOf( + "Pop" to { backStack.pop() }, + "Push" to { backStack.push(InteractionTarget.Element()) } + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = backStack, + actions = actions, + childSize = ChildSize.MAX, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/main.js.kt b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/main.js.kt new file mode 100644 index 000000000..810e353c1 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/main.js.kt @@ -0,0 +1,44 @@ +package com.bumble.appyx.demos.backstack.parallax + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + BackStackParallaxSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding(vertical = 16.dp) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts index d48bc8e78..c7f27e013 100644 --- a/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-backstack-slider-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/slider/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/slider/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/BackStackSliderSample.kt b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/BackStackSliderSample.kt new file mode 100644 index 000000000..22c4b1571 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/BackStackSliderSample.kt @@ -0,0 +1,52 @@ +package com.bumble.appyx.demos.backstack.slider + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.backstack.BackStack +import com.bumble.appyx.components.backstack.BackStackModel +import com.bumble.appyx.components.backstack.operation.pop +import com.bumble.appyx.components.backstack.operation.push +import com.bumble.appyx.components.backstack.ui.slider.BackStackSlider +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.ChildSize +import com.bumble.appyx.demos.common.InteractionTarget +import com.bumble.appyx.interactions.gesture.GestureFactory + +@Composable +fun BackStackSliderSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + BackStackModel( + initialTarget = InteractionTarget.Element(), + savedStateMap = null + ) + } + val backStack = + BackStack( + scope = coroutineScope, + model = model, + visualisation = { BackStackSlider(it) }, + gestureFactory = { GestureFactory.Noop() }, + animationSpec = spring(stiffness = Spring.StiffnessVeryLow), + ) + val actions = mapOf( + "Pop" to { backStack.pop() }, + "Push" to { backStack.push(InteractionTarget.Element()) } + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = backStack, + actions = actions, + childSize = ChildSize.MAX, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/main.js.kt b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/main.js.kt new file mode 100644 index 000000000..997896eca --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.backstack.slider + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + BackStackSliderSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts index fc9946580..91685b189 100644 --- a/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-backstack-stack3d-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/stack3d/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/BackStack3DSample.kt b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/BackStack3DSample.kt new file mode 100644 index 000000000..9c31d253a --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/BackStack3DSample.kt @@ -0,0 +1,51 @@ +package com.bumble.appyx.demos.backstack.stack3d + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.backstack.BackStack +import com.bumble.appyx.components.backstack.BackStackModel +import com.bumble.appyx.components.backstack.operation.pop +import com.bumble.appyx.components.backstack.operation.push +import com.bumble.appyx.components.backstack.ui.stack3d.BackStack3D +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.ChildSize +import com.bumble.appyx.demos.common.InteractionTarget + +@Composable +fun BackStack3DSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + BackStackModel( + initialTarget = InteractionTarget.Element(), + savedStateMap = null + ) + } + val backStack = + BackStack( + scope = coroutineScope, + model = model, + visualisation = { BackStack3D(it) }, + gestureFactory = { BackStack3D.Gestures(it) }, + animationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2), + ) + val actions = mapOf( + "Pop" to { backStack.pop() }, + "Push" to { backStack.push(InteractionTarget.Element()) } + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = backStack, + actions = actions, + childSize = ChildSize.MEDIUM, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/main.js.kt b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/main.js.kt new file mode 100644 index 000000000..840b348dc --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.backstack.stack3d + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + BackStack3DSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/common/build.gradle.kts b/demos/mkdocs/appyx-components/common/build.gradle.kts index 6c4418afe..944b9bde7 100644 --- a/demos/mkdocs/appyx-components/common/build.gradle.kts +++ b/demos/mkdocs/appyx-components/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "demos-mkdocs-appyx-components-common-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -29,4 +45,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/common/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts b/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts index 3de9b88f3..b28e41caf 100644 --- a/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-experimental-datingcards-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/experimental/datingcards/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/DatingCardsSample.kt b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/DatingCardsSample.kt new file mode 100644 index 000000000..a2532ecb5 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/DatingCardsSample.kt @@ -0,0 +1,49 @@ +package com.bumble.appyx.demos.experimental.datingcards + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.experimental.cards.Cards +import com.bumble.appyx.components.experimental.cards.CardsModel +import com.bumble.appyx.components.experimental.cards.operation.like +import com.bumble.appyx.components.experimental.cards.operation.pass +import com.bumble.appyx.components.experimental.cards.ui.CardsVisualisation +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.ChildSize +import com.bumble.appyx.demos.common.InteractionTarget + +@Composable +fun DatingCardsSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val cards = remember { + Cards( + model = CardsModel( + initialItems = List(15) { InteractionTarget.Element(it) }, + savedStateMap = null + ), + visualisation = { CardsVisualisation(it) }, + gestureFactory = { CardsVisualisation.Gestures(it) }, + animateSettle = true + ) + } + + val animationSpec: AnimationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2) + val actions = mapOf( + "Pass" to { cards.pass(animationSpec = animationSpec) }, + "Like" to { cards.like(animationSpec = animationSpec) }, + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = cards, + actions = actions, + childSize = ChildSize.MAX, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/main.js.kt b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/main.js.kt new file mode 100644 index 000000000..41c268dee --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.experimental.datingcards + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + DatingCardsSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts b/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts index 40d079ae9..8f6739d1f 100644 --- a/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-experimental-puzzle15-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/puzzle15/main.js.kt b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/puzzle15/main.js.kt new file mode 100644 index 000000000..ab71d5098 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/puzzle15/main.js.kt @@ -0,0 +1,50 @@ +package com.bumble.appyx.demos.experimental.puzzle15 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.components.experimental.puzzle15.ui.Puzzle15Ui +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark +import com.bumble.appyx.demos.common.color_primary + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + Puzzle15Ui( + screenWidthPx = size.width, + screenHeightPx = size.height, + accentColor = color_primary, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts index 89894921e..5b40e1c9a 100644 --- a/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-components-spotlight-fader-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/fader/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/fader/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/SpotlightFaderSample.kt b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/SpotlightFaderSample.kt new file mode 100644 index 000000000..1f1866f45 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/SpotlightFaderSample.kt @@ -0,0 +1,65 @@ +package com.bumble.appyx.demos.spotlight.fader + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.spotlight.Spotlight +import com.bumble.appyx.components.spotlight.SpotlightModel +import com.bumble.appyx.components.spotlight.operation.next +import com.bumble.appyx.components.spotlight.operation.previous +import com.bumble.appyx.components.spotlight.ui.fader.SpotlightFader +import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.ChildSize +import com.bumble.appyx.demos.common.InteractionTarget +import com.bumble.appyx.interactions.model.transition.Operation + +@Composable +fun SpotlightFaderSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + SpotlightModel( + items = List(7) { InteractionTarget.Element(it) }, + initialActiveIndex = 0f, + savedStateMap = null + ) + } + val spotlight = + Spotlight( + scope = coroutineScope, + model = model, + visualisation = { SpotlightFader(it) }, + gestureFactory = { SpotlightSlider.Gestures(it) }, + ) + val animationSpec: AnimationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2) + val actions = mapOf( + "Prev" to { + spotlight.previous( + mode = Operation.Mode.KEYFRAME, + animationSpec = animationSpec, + ) + }, + "Next" to { + spotlight.next( + mode = Operation.Mode.KEYFRAME, + animationSpec = animationSpec, + ) + }, + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = spotlight, + actions = actions, + childSize = ChildSize.MAX, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/main.js.kt b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/main.js.kt new file mode 100644 index 000000000..2c7277c71 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.spotlight.fader + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + SpotlightFaderSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 60.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts index 0626d9ca4..48fd78ee9 100644 --- a/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-components-spotlight-slider-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/slider/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/slider/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/SpotlightSliderSample.kt b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/SpotlightSliderSample.kt new file mode 100644 index 000000000..2ad4a672b --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/SpotlightSliderSample.kt @@ -0,0 +1,51 @@ +package com.bumble.appyx.demos.spotlight.slider + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.spotlight.Spotlight +import com.bumble.appyx.components.spotlight.SpotlightModel +import com.bumble.appyx.components.spotlight.operation.first +import com.bumble.appyx.components.spotlight.operation.last +import com.bumble.appyx.components.spotlight.operation.next +import com.bumble.appyx.components.spotlight.operation.previous +import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.InteractionTarget + +@Composable +fun SpotlightSliderSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + SpotlightModel( + items = List(7) { InteractionTarget.Element(it) }, + initialActiveIndex = 1f, + savedStateMap = null + ) + } + val spotlight = + Spotlight( + scope = coroutineScope, + model = model, + visualisation = { SpotlightSlider(it, model.currentState) }, + gestureFactory = { SpotlightSlider.Gestures(it) } + ) + val actions = mapOf( + "First" to { spotlight.first() }, + "Prev" to { spotlight.previous() }, + "Next" to { spotlight.next() }, + "Last" to { spotlight.last() }, + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = spotlight, + actions = actions, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/main.js.kt b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/main.js.kt new file mode 100644 index 000000000..eb8f4b292 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.spotlight.slider + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + SpotlightSliderSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 60.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts index b08345ee8..975eab2dd 100644 --- a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-components-spotlight-slider-rotation-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/SpotlightSliderRotationSample.kt b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/SpotlightSliderRotationSample.kt new file mode 100644 index 000000000..2fcb17043 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/SpotlightSliderRotationSample.kt @@ -0,0 +1,53 @@ +package com.bumble.appyx.demos.spotlight.sliderrotation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.spotlight.Spotlight +import com.bumble.appyx.components.spotlight.SpotlightModel +import com.bumble.appyx.components.spotlight.operation.first +import com.bumble.appyx.components.spotlight.operation.last +import com.bumble.appyx.components.spotlight.operation.next +import com.bumble.appyx.components.spotlight.operation.previous +import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider +import com.bumble.appyx.components.spotlight.ui.sliderrotation.SpotlightSliderRotation +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.InteractionTarget + +@Composable +fun SpotlightSliderRotationSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + SpotlightModel( + items = List(7) { InteractionTarget.Element(it) }, + initialActiveIndex = 1f, + savedStateMap = null + ) + } + val spotlight = + Spotlight( + scope = coroutineScope, + model = model, + visualisation = { SpotlightSliderRotation(it, model.currentState) }, + gestureFactory = { SpotlightSlider.Gestures(it) } + ) + val actions = mapOf( + "First" to { spotlight.first() }, + "Prev" to { spotlight.previous() }, + "Next" to { spotlight.next() }, + "Last" to { spotlight.last() }, + + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = spotlight, + actions = actions, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/main.js.kt b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/main.js.kt new file mode 100644 index 000000000..6999e1c8d --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.spotlight.sliderrotation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + SpotlightSliderRotationSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 85.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts index c58816d23..5269df2ab 100644 --- a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-components-spotlight-slider-scale-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/SpotlightSliderScaleSample.kt b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/SpotlightSliderScaleSample.kt new file mode 100644 index 000000000..8c1e8baba --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/SpotlightSliderScaleSample.kt @@ -0,0 +1,52 @@ +package com.bumble.appyx.demos.spotlight.sliderscale + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.spotlight.Spotlight +import com.bumble.appyx.components.spotlight.SpotlightModel +import com.bumble.appyx.components.spotlight.operation.first +import com.bumble.appyx.components.spotlight.operation.last +import com.bumble.appyx.components.spotlight.operation.next +import com.bumble.appyx.components.spotlight.operation.previous +import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider +import com.bumble.appyx.components.spotlight.ui.sliderscale.SpotlightSliderScale +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.InteractionTarget + +@Composable +fun SpotlightSliderScaleSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + SpotlightModel( + items = List(7) { InteractionTarget.Element(it) }, + initialActiveIndex = 1f, + savedStateMap = null + ) + } + val spotlight = + Spotlight( + scope = coroutineScope, + model = model, + visualisation = { SpotlightSliderScale(it, model.currentState) }, + gestureFactory = { SpotlightSlider.Gestures(it) } + ) + val actions = mapOf( + "First" to { spotlight.first() }, + "Prev" to { spotlight.previous() }, + "Next" to { spotlight.next() }, + "Last" to { spotlight.last() }, + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = spotlight, + actions = actions, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/main.js.kt b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/main.js.kt new file mode 100644 index 000000000..72ed61307 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.spotlight.sliderscale + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + SpotlightSliderScaleSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 60.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts index 27eaa27ad..7e19b4aae 100644 --- a/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts +++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-components-spotlight-stack3d-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/stack3d/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/SpotlightStack3DSample.kt b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/SpotlightStack3DSample.kt new file mode 100644 index 000000000..12279a8ce --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/SpotlightStack3DSample.kt @@ -0,0 +1,72 @@ +package com.bumble.appyx.demos.spotlight.stack3d + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.bumble.appyx.components.spotlight.Spotlight +import com.bumble.appyx.components.spotlight.SpotlightModel +import com.bumble.appyx.components.spotlight.operation.next +import com.bumble.appyx.components.spotlight.operation.previous +import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider +import com.bumble.appyx.components.spotlight.ui.stack3d.SpotlightStack3D +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.ChildSize +import com.bumble.appyx.demos.common.InteractionTarget +import com.bumble.appyx.interactions.model.transition.Operation + +@Composable +fun SpotlightStack3DSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + SpotlightModel( + items = List(7) { InteractionTarget.Element(it) }, + initialActiveIndex = 0f, + savedStateMap = null + ) + } + val spotlight = + Spotlight( + scope = coroutineScope, + model = model, + visualisation = { SpotlightStack3D(it, model.currentState) }, + gestureFactory = { + SpotlightSlider.Gestures( + transitionBounds = it, + orientation = Orientation.Vertical, + reverseOrientation = true, + ) + }, + ) + val animationSpec: AnimationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2) + val actions = mapOf( + "Prev" to { + spotlight.previous( + mode = Operation.Mode.KEYFRAME, + animationSpec = animationSpec, + ) + }, + "Next" to { + spotlight.next( + mode = Operation.Mode.KEYFRAME, + animationSpec = animationSpec, + ) + }, + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = spotlight, + actions = actions, + childSize = ChildSize.MEDIUM, + modifier = modifier, + ) +} diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/main.js.kt b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/main.js.kt new file mode 100644 index 000000000..39f40cc69 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.spotlight.stack3d + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample +import com.bumble.appyx.demos.common.color_dark + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + SpotlightStack3DSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 60.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts index c8c76e0b4..cefebc6a7 100644 --- a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-interactions-gestures-dragpredication-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPrediction.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPrediction.kt new file mode 100644 index 000000000..563c27997 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPrediction.kt @@ -0,0 +1,230 @@ +@file:Suppress("MatchingDeclarationName") + +package com.bumble.appyx.demos.dragprediction + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.bumble.appyx.components.internal.testdrive.TestDrive +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.demos.dragprediction.DragPredictionVisualisation.Companion.toTargetUiState +import com.bumble.appyx.demos.dragprediction.InteractionTarget.Child1 +import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer +import com.bumble.appyx.interactions.model.transition.Keyframes +import com.bumble.appyx.interactions.model.transition.Update +import com.bumble.appyx.interactions.gesture.GestureSettleConfig +import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup + +enum class InteractionTarget { + Child1 +} + +@Composable +fun DragPrediction( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { TestDriveModel(Child1, null) } + val testDrive = remember { + TestDrive( + scope = coroutineScope, + model = model, + progressAnimationSpec = spring(visibilityThreshold = 0.001f), + visualisation = { DragPredictionVisualisation(it) }, + gestureFactory = { DragPredictionVisualisation.Gestures(it) }, + gestureSettleConfig = GestureSettleConfig(0.25f) + ) + } + + AppyxComponentSetup(testDrive) + + val output = model.output.collectAsState().value + val currentTarget: androidx.compose.runtime.State?> = + when (output) { + is Keyframes -> output.currentSegmentTargetStateFlow.collectAsState(null) + is Update -> remember(output) { mutableStateOf(output.currentTargetState) } + } + + @Suppress("UnusedPrivateMember") + val index = when (output) { + is Keyframes -> output.currentIndex + is Update -> null + } + + Box( + modifier = modifier, + ) { + Background( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + currentTarget = currentTarget.value + ) + Box( + modifier = Modifier.padding(24.dp, 24.dp) + ) { + Target(elementState = B, alpha = 0.15f) + Target(elementState = C, alpha = 0.15f) + Target(elementState = D, alpha = 0.15f) + Target(elementState = currentTarget.value?.elementState, alpha = 0.65f) + ModelUi( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + testDrive = testDrive, + model = model + ) + Controls( + testDrive = testDrive + ) + } + } +} + +@Composable +fun Background( + screenWidthPx: Int, + screenHeightPx: Int, + currentTarget: TestDriveModel.State?, + modifier: Modifier = Modifier.fillMaxSize() +) { + val backgroundColor1 = animateColorAsState( + when (currentTarget?.elementState) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + null -> color_bright + }, + animationSpec = spring(stiffness = Spring.StiffnessVeryLow) + ) + + Box( + modifier + .zIndex(0f) + .background( + ShaderBrush( + LinearGradientShader( + from = Offset.Zero, + to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()), + colors = listOf(color_bright, backgroundColor1.value) + ) + ) + ) + ) +} + +@Composable +fun Target( + elementState: ElementState?, + alpha: Float, + modifier: Modifier = Modifier, +) { + val targetUiState = elementState?.toTargetUiState() + targetUiState?.let { + Box( + modifier = modifier + .size(60.dp) + .offset( + targetUiState.positionOffset.value.offset.x, + targetUiState.positionOffset.value.offset.y + ) + .scale(targetUiState.scale.value) + .rotate(targetUiState.rotationZ.value) + .alpha(alpha) + .background( + color = targetUiState.backgroundColor.value, + shape = RoundedCornerShape(targetUiState.roundedCorners.value) + ) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = elementState.name, + fontSize = 24.sp, + color = Color.White + ) + } + } +} + +@Composable +fun ModelUi( + screenWidthPx: Int, + screenHeightPx: Int, + testDrive: TestDrive, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + AppyxInteractionsContainer( + appyxComponent = testDrive, + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + modifier = modifier + ) { + Box( + modifier = Modifier.size(60.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = model.output.collectAsState().value.currentTargetState.elementState.name, + fontSize = 24.sp, + color = Color.White + ) + } + } +} + +@Suppress("UnusedPrivateMember") +@Composable +private fun Controls( + testDrive: TestDrive, + modifier: Modifier = Modifier.fillMaxSize() +) { + Column( + modifier = modifier.zIndex(1f), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = CenterHorizontally + ) { + Row { + Spacer(Modifier.width(16.dp)) + } + + } +} diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPredictionVisualisation.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPredictionVisualisation.kt new file mode 100644 index 000000000..66caff8c7 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPredictionVisualisation.kt @@ -0,0 +1,136 @@ +package com.bumble.appyx.demos.dragprediction + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.MoveTo +import com.bumble.appyx.interactions.ui.context.TransitionBounds +import com.bumble.appyx.interactions.ui.context.UiContext +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT +import com.bumble.appyx.interactions.gesture.Gesture +import com.bumble.appyx.interactions.gesture.GestureFactory +import com.bumble.appyx.interactions.gesture.dragDirection8 +import com.bumble.appyx.interactions.ui.DefaultAnimationSpec +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RotationZ +import com.bumble.appyx.interactions.ui.property.impl.Scale +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState +import com.bumble.appyx.transitionmodel.BaseVisualisation +import com.bumble.appyx.utils.multiplatform.AppyxLogger + +class DragPredictionVisualisation( + uiContext: UiContext, + uiAnimationSpec: SpringSpec = DefaultAnimationSpec +) : BaseVisualisation, TargetUiState, MutableUiState>( + uiContext = uiContext, + defaultAnimationSpec = uiAnimationSpec, +) { + override fun TestDriveModel.State.toUiTargets(): + List> = + listOf( + MatchedTargetUiState(element, elementState.toTargetUiState()).also { + AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}") + } + ) + + companion object { + fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState = + when (this) { + A -> topLeftCorner + B -> topRightCorner + C -> BottomRightCorner + D -> bottomLeftCorner + } + + private val topLeftCorner = TargetUiState( + positionOffset = PositionOffset.Target(DpOffset(0.dp, 0.dp)), + scale = Scale.Target(1f), + backgroundColor = BackgroundColor.Target(color_primary) + ) + + private val topRightCorner = TargetUiState( + positionOffset = PositionOffset.Target(DpOffset(180.dp, 30.dp)), + scale = Scale.Target(2f, TransformOrigin(0f, 0f)), + backgroundColor = BackgroundColor.Target(color_dark) + ) + + private val BottomRightCorner = TargetUiState( + positionOffset = PositionOffset.Target(DpOffset(180.dp, 180.dp)), + scale = Scale.Target(2f, TransformOrigin(0f, 0f)), + rotationZ = RotationZ.Target(90f), + backgroundColor = BackgroundColor.Target(color_secondary) + ) + + private val bottomLeftCorner = TargetUiState( + positionOffset = PositionOffset.Target(DpOffset(30.dp, 180.dp)), + scale = Scale.Target(2f, TransformOrigin(0f, 0f)), + rotationZ = RotationZ.Target(180f), + backgroundColor = BackgroundColor.Target(color_tertiary) + ) + } + + override fun mutableUiStateFor( + uiContext: UiContext, + targetUiState: TargetUiState + ): MutableUiState = + targetUiState.toMutableUiState(uiContext) + + + @Suppress("UnusedPrivateMember") + class Gestures( + transitionBounds: TransitionBounds, + ) : GestureFactory> { + private val maxX = topRightCorner.positionOffset.value.offset.x - topLeftCorner.positionOffset.value.offset.x + private val maxY = bottomLeftCorner.positionOffset.value.offset.y - topLeftCorner.positionOffset.value.offset.y + + @Suppress("ComplexMethod") + override fun createGesture( + state: TestDriveModel.State, + delta: Offset, + density: Density + ): Gesture> { + val maxX = with(density) { maxX.toPx() } + val maxY = with(density) { maxY.toPx() } + + val direction = dragDirection8(delta) + return when (state.elementState) { + A -> when (direction) { + RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f)) + DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY)) + DOWN -> Gesture(MoveTo(D), Offset(0f, maxY)) + else -> Gesture.Noop() + } + + B -> when (direction) { + LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f)) + else -> Gesture.Noop() + } + + C -> when (direction) { + UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY)) + else -> Gesture.Noop() + } + + D -> when (direction) { + UP -> Gesture(MoveTo(A), Offset(0f, -maxY)) + else -> Gesture.Noop() + } + } + } + } +} + diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/TargetUiState.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/TargetUiState.kt new file mode 100644 index 000000000..e79f03f3a --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/TargetUiState.kt @@ -0,0 +1,18 @@ +package com.bumble.appyx.demos.dragprediction + +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RotationZ +import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners +import com.bumble.appyx.interactions.ui.property.impl.Scale +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs + +@Suppress("unused") +@MutableUiStateSpecs +class TargetUiState( + val positionOffset: PositionOffset.Target, + val scale: Scale.Target, + val rotationZ: RotationZ.Target = RotationZ.Target(0f), + val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10), + val backgroundColor: BackgroundColor.Target, +) diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/main.js.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/main.js.kt new file mode 100644 index 000000000..8cb6ad058 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/main.js.kt @@ -0,0 +1,57 @@ +package com.bumble.appyx.demos.dragprediction + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample + +val color_bright = Color(0xFFFFFFFF) +val color_dark = Color(0xFF353535) +val color_primary = Color(0xFFFFC629) +val color_secondary = Color(0xFFFE9763) +val color_tertiary = Color(0xFF855353) +val color_neutral1 = Color(0xFFD2D7DF) +val color_neutral2 = Color(0xFF8A897C) +val color_neutral3 = Color(0xFFD9E8ED) +val color_neutral4 = Color(0xFFBEA489) + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + DragPrediction( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts index 984bcaf61..d6c84aeda 100644 --- a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-interactions-gestures-incompletedrag-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -35,4 +51,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDrag.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDrag.kt new file mode 100644 index 000000000..064e9b164 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDrag.kt @@ -0,0 +1,194 @@ +@file:Suppress("MatchingDeclarationName") + +package com.bumble.appyx.demos.incompletedrag + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.bumble.appyx.components.internal.testdrive.TestDrive +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.next +import com.bumble.appyx.demos.incompletedrag.InteractionTarget.Child1 +import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer +import com.bumble.appyx.interactions.gesture.GestureReferencePoint +import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE +import com.bumble.appyx.interactions.gesture.GestureSettleConfig +import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup + +enum class InteractionTarget { + Child1 +} + +@Composable +fun IncompleteDrag( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { TestDriveModel(Child1, null) } + val testDrive = remember { + TestDrive( + scope = coroutineScope, + model = model, + visualisation = { IncompleteDragVisualisation(it) }, + gestureFactory = { IncompleteDragVisualisation.Gestures(it) }, + gestureSettleConfig = GestureSettleConfig(0.15f) + ) + } + + AppyxComponentSetup(testDrive) + + Box( + modifier = modifier, + ) { + Background( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + model = model + ) + Box( + modifier = Modifier.padding(24.dp, 24.dp) + ) { + ModelUi( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + testDrive = testDrive, + model = model + ) + Controls( + testDrive = testDrive + ) + } + } +} + +@Composable +fun Background( + screenWidthPx: Int, + screenHeightPx: Int, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + val output = model.output.collectAsState() + val currentTarget = output.value.currentTargetState.elementState + val backgroundColor1 = animateColorAsState( + when (currentTarget) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + } + ) + + Box( + modifier + .zIndex(0f) + .background( + ShaderBrush( + LinearGradientShader( + from = Offset.Zero, + to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()), + colors = listOf(color_bright, backgroundColor1.value) + ) + ) + ) + ) +} + +@Composable +fun ModelUi( + screenWidthPx: Int, + screenHeightPx: Int, + testDrive: TestDrive, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + val output = model.output.collectAsState() + val currentTarget = output.value.currentTargetState.elementState + val backgroundColor1 = animateColorAsState( + when (currentTarget) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + } + ) + + AppyxInteractionsContainer( + appyxComponent = testDrive, + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + gestureRelativeTo = GestureReferencePoint.Element, + modifier = modifier + ) + { + Box( + modifier = Modifier + .size(60.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = model.output.collectAsState().value.currentTargetState.elementState.name, + fontSize = 24.sp, + color = Color.White + ) + } + } +} + +@Composable +private fun Controls( + testDrive: TestDrive, + modifier: Modifier = Modifier.fillMaxSize() +) { + Column( + modifier = modifier.zIndex(1f), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = CenterHorizontally + ) { + Box( + modifier = Modifier + .background(color_primary, shape = RoundedCornerShape(4.dp)) + .clickable { + testDrive.next( + mode = IMMEDIATE, animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioLowBouncy, + ) + ) + } + .padding(horizontal = 18.dp, vertical = 9.dp) + ) { + Text("Next") + } + } +} diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDragVisualisation.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDragVisualisation.kt new file mode 100644 index 000000000..0716497d1 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDragVisualisation.kt @@ -0,0 +1,154 @@ +package com.bumble.appyx.demos.incompletedrag + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.MoveTo +import com.bumble.appyx.interactions.ui.context.TransitionBounds +import com.bumble.appyx.interactions.ui.context.UiContext +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT +import com.bumble.appyx.interactions.gesture.Gesture +import com.bumble.appyx.interactions.gesture.GestureFactory +import com.bumble.appyx.interactions.gesture.dragDirection8 +import com.bumble.appyx.interactions.ui.DefaultAnimationSpec +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RotationZ +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomStart +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState +import com.bumble.appyx.transitionmodel.BaseVisualisation +import com.bumble.appyx.utils.multiplatform.AppyxLogger + +class IncompleteDragVisualisation( + uiContext: UiContext, + uiAnimationSpec: SpringSpec = DefaultAnimationSpec +) : BaseVisualisation, TargetUiState, MutableUiState>( + uiContext = uiContext, + defaultAnimationSpec = uiAnimationSpec, +) { + override fun TestDriveModel.State.toUiTargets(): + List> = + listOf( + MatchedTargetUiState(element, elementState.toTargetUiState()).also { + AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}") + } + ) + + companion object { + val bottomOffset = DpOffset(0.dp, (-50).dp) + + fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState = + when (this) { + A -> topLeftCorner + B -> topRightCorner + C -> bottomRightCorner + D -> bottomLeftCorner + } + + private val topLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopStart), + positionOffset = PositionOffset.Target(DpOffset.Zero), + rotationZ = RotationZ.Target(0f), + backgroundColor = BackgroundColor.Target(color_primary) + ) + + private val topRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopEnd), + positionOffset = PositionOffset.Target(DpOffset.Zero), + rotationZ = RotationZ.Target(180f), + backgroundColor = BackgroundColor.Target(color_dark) + ) + + private val bottomRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(BottomEnd), + positionOffset = PositionOffset.Target(bottomOffset), + rotationZ = RotationZ.Target(270f), + backgroundColor = BackgroundColor.Target(color_secondary) + ) + + private val bottomLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(BottomStart), + positionOffset = PositionOffset.Target(bottomOffset), + rotationZ = RotationZ.Target(540f), + backgroundColor = BackgroundColor.Target(color_tertiary) + ) + } + + override fun mutableUiStateFor( + uiContext: UiContext, + targetUiState: TargetUiState + ): MutableUiState = + targetUiState.toMutableUiState(uiContext) + + + class Gestures( + private val transitionBounds: TransitionBounds, + ) : GestureFactory> { + + @Suppress("ComplexMethod") + override fun createGesture( + state: TestDriveModel.State, + delta: Offset, + density: Density + ): Gesture> { + // FIXME 60.dp is the assumed element size, connect it to real value + // TODO automate this whole calculation based on .onPlaced centers of targetUiStates + val maxX = with(density) { + (transitionBounds.widthDp - 60.dp).toPx() + } + val maxY = with(density) { + (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx() + } + + val direction = dragDirection8(delta) + return when (state.elementState) { + A -> when (direction) { + RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f)) + DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY)) + DOWN -> Gesture(MoveTo(D), Offset(0f, maxY)) + else -> Gesture.Noop() + } + + B -> when (direction) { + DOWN -> Gesture(MoveTo(C), Offset(0f, maxY)) + DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY)) + LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f)) + else -> Gesture.Noop() + } + + C -> when (direction) { + LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f)) + UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY)) + UP -> Gesture(MoveTo(B), Offset(0f, -maxY)) + else -> Gesture.Noop() + } + + D -> when (direction) { + UP -> Gesture(MoveTo(A), Offset(0f, -maxY)) + UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY)) + RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f)) + else -> Gesture.Noop() + } + } + } + } +} + diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/TargetUiState.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/TargetUiState.kt new file mode 100644 index 000000000..421759948 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/TargetUiState.kt @@ -0,0 +1,18 @@ +package com.bumble.appyx.demos.incompletedrag + +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RotationZ +import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs + +@Suppress("unused") +@MutableUiStateSpecs +class TargetUiState( + val positionAlignment: PositionAlignment.Target, + val positionOffset: PositionOffset.Target, + val rotationZ: RotationZ.Target, + val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10), + val backgroundColor: BackgroundColor.Target, +) diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/main.js.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/main.js.kt new file mode 100644 index 000000000..421193d13 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/main.js.kt @@ -0,0 +1,57 @@ +package com.bumble.appyx.demos.incompletedrag + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample + +val color_bright = Color(0xFFFFFFFF) +val color_dark = Color(0xFF353535) +val color_primary = Color(0xFFFFC629) +val color_secondary = Color(0xFFFE9763) +val color_tertiary = Color(0xFF855353) +val color_neutral1 = Color(0xFFD2D7DF) +val color_neutral2 = Color(0xFF8A897C) +val color_neutral3 = Color(0xFFD9E8ED) +val color_neutral4 = Color(0xFFBEA489) + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + IncompleteDrag( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..cb31659d7 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts index b34f2c892..1f3d46046 100644 --- a/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts +++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-interactions-observemp-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -31,4 +47,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/observemp/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/ObserveMotionPropertiesSample.kt b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/ObserveMotionPropertiesSample.kt new file mode 100644 index 000000000..3223c42a8 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/ObserveMotionPropertiesSample.kt @@ -0,0 +1,137 @@ +package com.bumble.appyx.demos.observemp + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bumble.appyx.components.spotlight.Spotlight +import com.bumble.appyx.components.spotlight.SpotlightModel +import com.bumble.appyx.components.spotlight.operation.first +import com.bumble.appyx.components.spotlight.operation.last +import com.bumble.appyx.components.spotlight.operation.next +import com.bumble.appyx.components.spotlight.operation.previous +import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider +import com.bumble.appyx.components.spotlight.ui.sliderrotation.SpotlightSliderRotation +import com.bumble.appyx.demos.common.AppyxWebSample +import com.bumble.appyx.demos.common.InteractionTarget +import com.bumble.appyx.demos.common.colors +import com.bumble.appyx.interactions.model.Element +import com.bumble.appyx.interactions.ui.property.impl.RotationY +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.motionPropertyRenderValue +import kotlin.math.roundToInt + +@Composable +fun ObserveMotionPropertiesSample( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { + SpotlightModel( + items = List(7) { InteractionTarget.Element(it) }, + initialActiveIndex = 1f, + savedStateMap = null + ) + } + val spotlight = remember { + Spotlight( + scope = coroutineScope, + model = model, + visualisation = { SpotlightSliderRotation(it, model.currentState) }, + gestureFactory = { SpotlightSlider.Gestures(it) } + ) + } + val actions = mapOf( + "First" to { spotlight.first() }, + "Prev" to { spotlight.previous() }, + "Next" to { spotlight.next() }, + "Last" to { spotlight.last() }, + + ) + AppyxWebSample( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + appyxComponent = spotlight, + actions = actions, + modifier = modifier, + ) { + ModalUi(it, false) + } +} + +@Composable +fun ModalUi( + element: Element, + isChildMaxSize: Boolean, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize(if (isChildMaxSize) 1f else 0.9f) + .background( + color = when (val target = element.interactionTarget) { + is com.bumble.appyx.demos.common.InteractionTarget.Element -> colors.getOrElse( + target.idx % colors.size + ) { Color.Cyan } + + else -> { + Color.Cyan + } + }, + shape = RoundedCornerShape(if (isChildMaxSize) 0 else 8) + ) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = element.interactionTarget.toString(), + fontSize = 12.sp, + color = Color.White + ) + val alignment = + motionPropertyRenderValue() + if (alignment != null) { + val offsetPercentage = roundFloatToTwoDecimals( + alignment.outsideAlignment.horizontalBias * 100 + ) + + Text( + text = "Offset:\n$offsetPercentage%", + fontSize = 12.sp, + textAlign = TextAlign.Center, + color = Color.White + ) + } + val rotationY = motionPropertyRenderValue() + if (rotationY != null) { + Text( + text = "Rotation:\n${roundFloatToTwoDecimals(rotationY)}°", + fontSize = 12.sp, + textAlign = TextAlign.Center, + color = Color.White + ) + } + } + } +} + +private fun roundFloatToTwoDecimals(float: Float): Double { + return (float * 100.0).roundToInt() / 100.0 +} diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/main.js.kt new file mode 100644 index 000000000..be0f3f002 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/main.js.kt @@ -0,0 +1,47 @@ +package com.bumble.appyx.demos.observemp + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.common.color_dark + +external fun onWasmReady(onReady: () -> Unit) + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + onWasmReady { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + ObserveMotionPropertiesSample( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 85.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..5ca3e007b --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts index ee1fbb5ef..94023af25 100644 --- a/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-interactions-sample1-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/sample1/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1.kt new file mode 100644 index 000000000..b6db13809 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1.kt @@ -0,0 +1,189 @@ +@file:Suppress("MatchingDeclarationName") +package com.bumble.appyx.demos.sample1 + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.bumble.appyx.components.internal.testdrive.TestDrive +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.next +import com.bumble.appyx.demos.sample1.InteractionTarget.Child1 +import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer +import com.bumble.appyx.interactions.gesture.GestureReferencePoint +import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE +import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup + +enum class InteractionTarget { + Child1 +} + +@Composable +fun Sample1( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { TestDriveModel(Child1, null) } + val testDrive = remember { + TestDrive( + scope = coroutineScope, + model = model, + visualisation = { Sample1Visualisation(it) }, + gestureFactory = { Sample1Visualisation.Gestures(it) } + ) + } + + AppyxComponentSetup(testDrive) + + Box( + modifier = modifier, + ) { + Background( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + model = model + ) + Box( + modifier = Modifier.padding(24.dp, 24.dp) + ) { + ModelUi( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + testDrive = testDrive, + model = model + ) + Controls( + testDrive = testDrive + ) + } + } +} + +@Composable +fun Background( + screenWidthPx: Int, + screenHeightPx: Int, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + val output = model.output.collectAsState() + val currentTarget = output.value.currentTargetState.elementState + val backgroundColor1 = animateColorAsState( + when (currentTarget) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + } + ) + + Box( + modifier + .zIndex(0f) + .background( + ShaderBrush( + LinearGradientShader( + from = Offset.Zero, + to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()), + colors = listOf(color_bright, backgroundColor1.value) + ) + ) + ) + ) +} + +@Composable +fun ModelUi( + screenWidthPx: Int, + screenHeightPx: Int, + testDrive: TestDrive, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + val output = model.output.collectAsState() + val currentTarget = output.value.currentTargetState.elementState + val backgroundColor1 = animateColorAsState( + when (currentTarget) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + } + ) + + AppyxInteractionsContainer( + appyxComponent = testDrive, + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + gestureRelativeTo = GestureReferencePoint.Element + ) { + Box( + modifier = Modifier + .size(60.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = model.output.collectAsState().value.currentTargetState.elementState.name, + fontSize = 24.sp, + color = Color.White + ) + } + } +} + +@Composable +private fun Controls( + testDrive: TestDrive, + modifier: Modifier = Modifier.fillMaxSize() +) { + Column( + modifier = modifier.zIndex(1f), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = CenterHorizontally + ) { + Box( + modifier = Modifier + .background(color_primary, shape = RoundedCornerShape(4.dp)) + .clickable { + testDrive.next( + mode = IMMEDIATE, animationSpec = spring( + stiffness = Spring.StiffnessMedium, + dampingRatio = Spring.DampingRatioLowBouncy, + ) + ) + } + .padding(horizontal = 18.dp, vertical = 9.dp) + ) { + Text("Next") + } + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1Visualisation.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1Visualisation.kt new file mode 100644 index 000000000..8fd3e1764 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1Visualisation.kt @@ -0,0 +1,148 @@ +package com.bumble.appyx.demos.sample1 + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.MoveTo +import com.bumble.appyx.interactions.ui.context.TransitionBounds +import com.bumble.appyx.interactions.ui.context.UiContext +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT +import com.bumble.appyx.interactions.gesture.Gesture +import com.bumble.appyx.interactions.gesture.GestureFactory +import com.bumble.appyx.interactions.gesture.dragDirection8 +import com.bumble.appyx.interactions.ui.DefaultAnimationSpec +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState +import com.bumble.appyx.transitionmodel.BaseVisualisation +import com.bumble.appyx.utils.multiplatform.AppyxLogger + +class Sample1Visualisation( + uiContext: UiContext, + uiAnimationSpec: SpringSpec = DefaultAnimationSpec +) : BaseVisualisation, TargetUiState, MutableUiState>( + uiContext = uiContext, + defaultAnimationSpec = uiAnimationSpec, +) { + override fun TestDriveModel.State.toUiTargets(): + List> = + listOf( + MatchedTargetUiState(element, elementState.toTargetUiState()).also { + AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}") + } + ) + + companion object { + val bottomOffset = DpOffset(0.dp, (-50).dp) + + fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState = + when (this) { + A -> topLeftCorner + B -> topRightCorner + C -> bottomRightCorner + D -> bottomLeftCorner + } + + private val topLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopStart), + positionOffset = PositionOffset.Target(DpOffset.Zero), + backgroundColor = BackgroundColor.Target(color_primary) + ) + + private val topRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopEnd), + positionOffset = PositionOffset.Target(DpOffset.Zero), + backgroundColor = BackgroundColor.Target(color_dark) + ) + + private val bottomRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(BottomEnd), + positionOffset = PositionOffset.Target(bottomOffset), + backgroundColor = BackgroundColor.Target(color_secondary) + ) + + private val bottomLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(InsideAlignment.BottomStart), + positionOffset = PositionOffset.Target(bottomOffset), + backgroundColor = BackgroundColor.Target(color_tertiary) + ) + } + + override fun mutableUiStateFor( + uiContext: UiContext, + targetUiState: TargetUiState + ): MutableUiState = + targetUiState.toMutableUiState(uiContext) + + class Gestures( + private val transitionBounds: TransitionBounds, + ) : GestureFactory> { + + @Suppress("ComplexMethod") + override fun createGesture( + state: TestDriveModel.State, + delta: Offset, + density: Density + ): Gesture> { + // FIXME quick fix – 60.dp is the assumed element size, connect it to real value + // TODO properly – automate this whole calculation based on .onPlaced centers of targetUiStates + val maxX = with(density) { + (transitionBounds.widthDp - 60.dp).toPx() + } + val maxY = with(density) { + (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx() + } + + val direction = dragDirection8(delta) + return when (state.elementState) { + A -> when (direction) { + RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f)) + DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY)) + DOWN -> Gesture(MoveTo(D), Offset(0f, maxY)) + else -> Gesture.Noop() + } + + B -> when (direction) { + DOWN -> Gesture(MoveTo(C), Offset(0f, maxY)) + DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY)) + LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f)) + else -> Gesture.Noop() + } + + C -> when (direction) { + LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f)) + UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY)) + UP -> Gesture(MoveTo(B), Offset(0f, -maxY)) + else -> Gesture.Noop() + } + + D -> when (direction) { + UP -> Gesture(MoveTo(A), Offset(0f, -maxY)) + UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY)) + RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f)) + else -> Gesture.Noop() + } + } + } + } +} + diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/TargetUiState.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/TargetUiState.kt new file mode 100644 index 000000000..4312ea1b6 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/TargetUiState.kt @@ -0,0 +1,16 @@ +package com.bumble.appyx.demos.sample1 + +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs + +@Suppress("unused") +@MutableUiStateSpecs +class TargetUiState( + val positionAlignment: PositionAlignment.Target, + val positionOffset: PositionOffset.Target, + val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10), + val backgroundColor: BackgroundColor.Target, +) diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/main.js.kt new file mode 100644 index 000000000..ecd275657 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/main.js.kt @@ -0,0 +1,57 @@ +package com.bumble.appyx.demos.sample1 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample + +val color_bright = Color(0xFFFFFFFF) +val color_dark = Color(0xFF353535) +val color_primary = Color(0xFFFFC629) +val color_secondary = Color(0xFFFE9763) +val color_tertiary = Color(0xFF855353) +val color_neutral1 = Color(0xFFD2D7DF) +val color_neutral2 = Color(0xFF8A897C) +val color_neutral3 = Color(0xFFD9E8ED) +val color_neutral4 = Color(0xFFBEA489) + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + Sample1( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..5ca3e007b --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts index 755a322db..d3ae7a978 100644 --- a/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-interactions-sample2-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/sample2/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2.kt new file mode 100644 index 000000000..be07cd535 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2.kt @@ -0,0 +1,190 @@ +@file:Suppress("MatchingDeclarationName") + +package com.bumble.appyx.demos.sample2 + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.bumble.appyx.components.internal.testdrive.TestDrive +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.next +import com.bumble.appyx.demos.sample2.InteractionTarget.Child1 +import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer +import com.bumble.appyx.interactions.gesture.GestureReferencePoint +import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE +import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup + +enum class InteractionTarget { + Child1 +} + +@Composable +fun Sample2( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { TestDriveModel(Child1, null) } + val testDrive = remember { + TestDrive( + scope = coroutineScope, + model = model, + visualisation = { Sample2Visualisation(it) }, + gestureFactory = { Sample2Visualisation.Gestures(it) } + ) + } + + AppyxComponentSetup(testDrive) + + Box( + modifier = modifier, + ) { + Background( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + model = model + ) + Box( + modifier = Modifier.padding(24.dp, 24.dp) + ) { + ModelUi( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + testDrive = testDrive, + model = model + ) + Controls( + testDrive = testDrive + ) + } + } +} + +@Composable +fun Background( + screenWidthPx: Int, + screenHeightPx: Int, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + val output = model.output.collectAsState() + val currentTarget = output.value.currentTargetState.elementState + val backgroundColor1 = animateColorAsState( + when (currentTarget) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + } + ) + + Box( + modifier + .zIndex(0f) + .background( + ShaderBrush( + LinearGradientShader( + from = Offset.Zero, + to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()), + colors = listOf(color_bright, backgroundColor1.value) + ) + ) + ) + ) +} + +@Composable +fun ModelUi( + screenWidthPx: Int, + screenHeightPx: Int, + testDrive: TestDrive, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + val output = model.output.collectAsState() + val currentTarget = output.value.currentTargetState.elementState + val backgroundColor1 = animateColorAsState( + when (currentTarget) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + } + ) + + AppyxInteractionsContainer( + appyxComponent = testDrive, + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + gestureRelativeTo = GestureReferencePoint.Element + ) { + Box( + modifier = Modifier + .size(60.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = model.output.collectAsState().value.currentTargetState.elementState.name, + fontSize = 24.sp, + color = Color.White + ) + } + } +} + +@Composable +private fun Controls( + testDrive: TestDrive, + modifier: Modifier = Modifier.fillMaxSize() +) { + Column( + modifier = modifier.zIndex(1f), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = CenterHorizontally + ) { + Box( + modifier = Modifier + .background(color_primary, shape = RoundedCornerShape(4.dp)) + .clickable { + testDrive.next( + mode = IMMEDIATE, animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioLowBouncy, + ) + ) + } + .padding(horizontal = 18.dp, vertical = 9.dp) + ) { + Text("Next") + } + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2Visualisation.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2Visualisation.kt new file mode 100644 index 000000000..2d7474fb3 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2Visualisation.kt @@ -0,0 +1,154 @@ +package com.bumble.appyx.demos.sample2 + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.MoveTo +import com.bumble.appyx.interactions.ui.context.TransitionBounds +import com.bumble.appyx.interactions.ui.context.UiContext +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT +import com.bumble.appyx.interactions.gesture.Gesture +import com.bumble.appyx.interactions.gesture.GestureFactory +import com.bumble.appyx.interactions.gesture.dragDirection8 +import com.bumble.appyx.interactions.ui.DefaultAnimationSpec +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RotationZ +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState +import com.bumble.appyx.transitionmodel.BaseVisualisation +import com.bumble.appyx.utils.multiplatform.AppyxLogger + +class Sample2Visualisation( + uiContext: UiContext, + uiAnimationSpec: SpringSpec = DefaultAnimationSpec +) : BaseVisualisation, TargetUiState, MutableUiState>( + uiContext = uiContext, + defaultAnimationSpec = uiAnimationSpec, +) { + override fun TestDriveModel.State.toUiTargets(): + List> = + listOf( + MatchedTargetUiState(element, elementState.toTargetUiState()).also { + AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}") + } + ) + + companion object { + val bottomOffset = DpOffset(0.dp, (-50).dp) + + fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState = + when (this) { + A -> topLeftCorner + B -> topRightCorner + C -> bottomRightCorner + D -> bottomLeftCorner + } + + private val topLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopStart), + positionOffset = PositionOffset.Target(DpOffset.Zero), + rotationZ = RotationZ.Target(0f), + backgroundColor = BackgroundColor.Target(color_primary) + ) + + private val topRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopEnd), + positionOffset = PositionOffset.Target(DpOffset.Zero), + rotationZ = RotationZ.Target(180f), + backgroundColor = BackgroundColor.Target(color_dark) + ) + + private val bottomRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(BottomEnd), + positionOffset = PositionOffset.Target(bottomOffset), + rotationZ = RotationZ.Target(270f), + backgroundColor = BackgroundColor.Target(color_secondary) + ) + + private val bottomLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(InsideAlignment.BottomStart), + positionOffset = PositionOffset.Target(bottomOffset), + rotationZ = RotationZ.Target(540f), + backgroundColor = BackgroundColor.Target(color_tertiary) + ) + } + + override fun mutableUiStateFor( + uiContext: UiContext, + targetUiState: TargetUiState + ): MutableUiState = + targetUiState.toMutableUiState(uiContext) + + + class Gestures( + private val transitionBounds: TransitionBounds, + ) : GestureFactory> { + + @Suppress("ComplexMethod") + override fun createGesture( + state: TestDriveModel.State, + delta: Offset, + density: Density + ): Gesture> { + // FIXME 60.dp is the assumed element size, connect it to real value + // TODO automate this whole calculation based on .onPlaced centers of targetUiStates + val maxX = with(density) { + (transitionBounds.widthDp - 60.dp).toPx() + } + val maxY = with(density) { + (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx() + } + + val direction = dragDirection8(delta) + return when (state.elementState) { + A -> when (direction) { + RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f)) + DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY)) + DOWN -> Gesture(MoveTo(D), Offset(0f, maxY)) + else -> Gesture.Noop() + } + + B -> when (direction) { + DOWN -> Gesture(MoveTo(C), Offset(0f, maxY)) + DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY)) + LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f)) + else -> Gesture.Noop() + } + + C -> when (direction) { + LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f)) + UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY)) + UP -> Gesture(MoveTo(B), Offset(0f, -maxY)) + else -> Gesture.Noop() + } + + D -> when (direction) { + UP -> Gesture(MoveTo(A), Offset(0f, -maxY)) + UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY)) + RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f)) + else -> Gesture.Noop() + } + } + } + } +} + diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/TargetUiState.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/TargetUiState.kt new file mode 100644 index 000000000..bd86cc384 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/TargetUiState.kt @@ -0,0 +1,18 @@ +package com.bumble.appyx.demos.sample2 + +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RotationZ +import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs + +@Suppress("unused") +@MutableUiStateSpecs +class TargetUiState( + val positionAlignment: PositionAlignment.Target, + val positionOffset: PositionOffset.Target, + val rotationZ: RotationZ.Target, + val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10), + val backgroundColor: BackgroundColor.Target, +) diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/main.js.kt new file mode 100644 index 000000000..5f074c3c2 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/main.js.kt @@ -0,0 +1,57 @@ +package com.bumble.appyx.demos.sample2 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample + +val color_bright = Color(0xFFFFFFFF) +val color_dark = Color(0xFF353535) +val color_primary = Color(0xFFFFC629) +val color_secondary = Color(0xFFFE9763) +val color_tertiary = Color(0xFF855353) +val color_neutral1 = Color(0xFFD2D7DF) +val color_neutral2 = Color(0xFF8A897C) +val color_neutral3 = Color(0xFFD9E8ED) +val color_neutral4 = Color(0xFFBEA489) + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + Sample2( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..5ca3e007b --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts index dadf32aa0..192ffe498 100644 --- a/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -10,6 +12,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-interactions-sample3-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { @@ -36,4 +52,5 @@ compose.experimental { dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) } diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/sample3/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3.kt new file mode 100644 index 000000000..c94fa7ad5 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3.kt @@ -0,0 +1,257 @@ +@file:Suppress("MatchingDeclarationName") +package com.bumble.appyx.demos.sample3 + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.bumble.appyx.components.internal.testdrive.TestDrive +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.next +import com.bumble.appyx.demos.sample3.InteractionTarget.Child1 +import com.bumble.appyx.demos.sample3.Sample3Visualisation.Companion.toTargetUiState +import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer +import com.bumble.appyx.interactions.gesture.GestureReferencePoint +import com.bumble.appyx.interactions.model.transition.Keyframes +import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE +import com.bumble.appyx.interactions.model.transition.Operation.Mode.KEYFRAME +import com.bumble.appyx.interactions.model.transition.Update +import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup + +enum class InteractionTarget { + Child1 +} + +@Composable +fun Sample3( + screenWidthPx: Int, + screenHeightPx: Int, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val model = remember { TestDriveModel(Child1, null) } + val testDrive = remember { + TestDrive( + scope = coroutineScope, + model = model, + progressAnimationSpec = spring( + stiffness = Spring.StiffnessVeryLow / 10, + visibilityThreshold = 0.001f + ), + visualisation = { Sample3Visualisation(it) }, + gestureFactory = { Sample3Visualisation.Gestures(it) } + ) + } + + AppyxComponentSetup(testDrive) + + val output = model.output.collectAsState().value + val currentTarget: State?> = + when (output) { + is Keyframes -> output.currentSegmentTargetStateFlow.collectAsState(null) + is Update -> remember(output) { mutableStateOf(output.currentTargetState) } + } + val index = when (output) { + is Keyframes -> output.currentIndex + is Update -> null + } + + Box( + modifier = modifier, + ) { + Background( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + currentTarget = currentTarget.value + ) + Box( + modifier = Modifier.padding(24.dp, 24.dp) + ) { + Target( + boxScope = this, + currentTarget = currentTarget.value, + index = index + ) + ModelUi( + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + testDrive = testDrive, + model = model + ) + Controls( + testDrive = testDrive + ) + } + } +} + +@Composable +fun Background( + screenWidthPx: Int, + screenHeightPx: Int, + currentTarget: TestDriveModel.State?, + modifier: Modifier = Modifier.fillMaxSize() +) { + val backgroundColor1 = animateColorAsState( + when (currentTarget?.elementState) { + A -> color_neutral1 + B -> color_neutral2 + C -> color_neutral3 + D -> color_neutral4 + null -> color_bright + }, + animationSpec = spring(stiffness = Spring.StiffnessVeryLow) + ) + + Box( + modifier + .zIndex(0f) + .background( + ShaderBrush( + LinearGradientShader( + from = Offset.Zero, + to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()), + colors = listOf(color_bright, backgroundColor1.value) + ) + ) + ) + ) +} + +@Composable +fun Target( + boxScope: BoxScope, + currentTarget: TestDriveModel.State?, + index: Int?, + modifier: Modifier = Modifier +) { + val targetUiState = currentTarget?.elementState?.toTargetUiState() + targetUiState?.let { + Box( + modifier = with(boxScope) { + modifier + .size(60.dp) + .align(targetUiState.positionAlignment.value) + .offset( + x = targetUiState.positionOffset.value.offset.x, + y = targetUiState.positionOffset.value.offset.y + ) + .alpha(0.35f) + .background( + color = targetUiState.backgroundColor.value, + shape = RoundedCornerShape(targetUiState.roundedCorners.value) + ) + } + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = index?.toString() ?: "X", + fontSize = 24.sp, + color = Color.White + ) + } + } +} + +@Composable +fun ModelUi( + screenWidthPx: Int, + screenHeightPx: Int, + testDrive: TestDrive, + model: TestDriveModel, + modifier: Modifier = Modifier.fillMaxSize() +) { + AppyxInteractionsContainer( + appyxComponent = testDrive, + screenWidthPx = screenWidthPx, + screenHeightPx = screenHeightPx, + gestureRelativeTo = GestureReferencePoint.Element, + modifier = modifier + ) { + Box( + modifier = Modifier.size(60.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = model.output.collectAsState().value.currentTargetState.elementState.name, + fontSize = 24.sp, + color = Color.White + ) + } + } +} + +@Composable +private fun Controls( + testDrive: TestDrive, + modifier: Modifier = Modifier.fillMaxSize() +) { + Column( + modifier = modifier.zIndex(1f), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = CenterHorizontally + ) { + Row { + Box( + modifier = Modifier + .background(color_primary, shape = RoundedCornerShape(4.dp)) + .clickable { testDrive.next(mode = KEYFRAME) } + .padding(horizontal = 18.dp, vertical = 9.dp) + ) { + Text("Keyframe") + } + Spacer(Modifier.width(16.dp)) + Box( + modifier = Modifier + .background(color_primary, shape = RoundedCornerShape(4.dp)) + .clickable { + testDrive.next( + mode = IMMEDIATE, spring( + stiffness = Spring.StiffnessVeryLow, + dampingRatio = Spring.DampingRatioMediumBouncy + ) + ) + } + .padding(horizontal = 18.dp, vertical = 9.dp) + ) { + Text("Immediate") + } + } + + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3Visualisation.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3Visualisation.kt new file mode 100644 index 000000000..00dd57a56 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3Visualisation.kt @@ -0,0 +1,148 @@ +package com.bumble.appyx.demos.sample3 + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.bumble.appyx.components.internal.testdrive.TestDriveModel +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C +import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D +import com.bumble.appyx.components.internal.testdrive.operation.MoveTo +import com.bumble.appyx.interactions.ui.context.TransitionBounds +import com.bumble.appyx.interactions.ui.context.UiContext +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT +import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT +import com.bumble.appyx.interactions.gesture.Gesture +import com.bumble.appyx.interactions.gesture.GestureFactory +import com.bumble.appyx.interactions.gesture.dragDirection8 +import com.bumble.appyx.interactions.ui.DefaultAnimationSpec +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomStart +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd +import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState +import com.bumble.appyx.transitionmodel.BaseVisualisation +import com.bumble.appyx.utils.multiplatform.AppyxLogger + +class Sample3Visualisation( + uiContext: UiContext, + uiAnimationSpec: SpringSpec = DefaultAnimationSpec +) : BaseVisualisation, TargetUiState, MutableUiState>( + uiContext = uiContext, + defaultAnimationSpec = uiAnimationSpec, +) { + override fun TestDriveModel.State.toUiTargets(): + List> = + listOf( + MatchedTargetUiState(element, elementState.toTargetUiState()).also { + AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}") + } + ) + + companion object { + val bottomOffset = DpOffset(0.dp, (-50).dp) + + fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState = + when (this) { + A -> topLeftCorner + B -> topRightCorner + C -> bottomRightCorner + D -> bottomLeftCorner + } + + private val topLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopStart), + positionOffset = PositionOffset.Target(DpOffset.Zero), + backgroundColor = BackgroundColor.Target(color_primary) + ) + + private val topRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(TopEnd), + positionOffset = PositionOffset.Target(DpOffset.Zero), + backgroundColor = BackgroundColor.Target(color_dark) + ) + + private val bottomRightCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(BottomEnd), + positionOffset = PositionOffset.Target(bottomOffset), + backgroundColor = BackgroundColor.Target(color_secondary) + ) + + private val bottomLeftCorner = TargetUiState( + positionAlignment = PositionAlignment.Target(BottomStart), + positionOffset = PositionOffset.Target(bottomOffset), + backgroundColor = BackgroundColor.Target(color_tertiary) + ) + } + + override fun mutableUiStateFor( + uiContext: UiContext, + targetUiState: TargetUiState + ): MutableUiState = + targetUiState.toMutableUiState(uiContext) + + class Gestures( + private val transitionBounds: TransitionBounds, + ) : GestureFactory> { + + @Suppress("ComplexMethod") + override fun createGesture( + state: TestDriveModel.State, + delta: Offset, + density: Density + ): Gesture> { + // FIXME 60.dp is the assumed element size, connect it to real value + // TODO automate this whole calculation based on .onPlaced centers of targetUiStates + val maxX = with(density) { + (transitionBounds.widthDp - 60.dp).toPx() + } + val maxY = with(density) { + (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx() + } + + val direction = dragDirection8(delta) + return when (state.elementState) { + A -> when (direction) { + RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f)) + DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY)) + DOWN -> Gesture(MoveTo(D), Offset(0f, maxY)) + else -> Gesture.Noop() + } + + B -> when (direction) { + DOWN -> Gesture(MoveTo(C), Offset(0f, maxY)) + DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY)) + LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f)) + else -> Gesture.Noop() + } + + C -> when (direction) { + LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f)) + UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY)) + UP -> Gesture(MoveTo(B), Offset(0f, -maxY)) + else -> Gesture.Noop() + } + + D -> when (direction) { + UP -> Gesture(MoveTo(A), Offset(0f, -maxY)) + UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY)) + RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f)) + else -> Gesture.Noop() + } + } + } + } +} + diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/TargetUiState.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/TargetUiState.kt new file mode 100644 index 000000000..9e00c3688 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/TargetUiState.kt @@ -0,0 +1,16 @@ +package com.bumble.appyx.demos.sample3 + +import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor +import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners +import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment +import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset +import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs + +@Suppress("unused") +@MutableUiStateSpecs +class TargetUiState( + val positionAlignment: PositionAlignment.Target, + val positionOffset: PositionOffset.Target, + val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10), + val backgroundColor: BackgroundColor.Target, +) diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/main.js.kt new file mode 100644 index 000000000..ae3c04bf4 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/main.js.kt @@ -0,0 +1,57 @@ +package com.bumble.appyx.demos.sample3 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.appyxSample + +val color_bright = Color(0xFFFFFFFF) +val color_dark = Color(0xFF353535) +val color_primary = Color(0xFFFFC629) +val color_secondary = Color(0xFFFE9763) +val color_tertiary = Color(0xFF855353) +val color_neutral1 = Color(0xFFD2D7DF) +val color_neutral2 = Color(0xFF8A897C) +val color_neutral3 = Color(0xFFD9E8ED) +val color_neutral4 = Color(0xFFBEA489) + + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + appyxSample { + CanvasBasedWindow("Appyx") { + var size by remember { mutableStateOf(IntSize.Zero) } + Surface( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { size = it } + ) { + if (size != IntSize.Zero) { + Sample3( + screenWidthPx = size.width, + screenHeightPx = size.height, + modifier = Modifier + .fillMaxSize() + .background(color_dark) + .padding( + horizontal = 16.dp, + vertical = 16.dp + ) + ) + } + } + } + } +} diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..5ca3e007b --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Appyx Interactions + + + + +
+ +
+ + + diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..f8b13d234 --- /dev/null +++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,19 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} + +@media (max-width: 511px) { + #ComposeTarget { + scale: 0.5; + transform-origin: 0 0; + } +} diff --git a/demos/mkdocs/common/build.gradle.kts b/demos/mkdocs/common/build.gradle.kts index 1bb32ce51..e92a3c463 100644 --- a/demos/mkdocs/common/build.gradle.kts +++ b/demos/mkdocs/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -9,6 +11,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "demos-mkdocs-common-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { diff --git a/demos/mkdocs/common/karma.config.d/wasm/config.js b/demos/mkdocs/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/mkdocs/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/mkdocs/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/AppyxSample.kt b/demos/mkdocs/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/AppyxSample.kt new file mode 100644 index 000000000..6386e9029 --- /dev/null +++ b/demos/mkdocs/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/AppyxSample.kt @@ -0,0 +1,65 @@ +package com.bumble.appyx.demos + +import kotlinx.browser.document +import org.w3c.dom.get + +private val LOADER_STYLES = """ + .loader { + width: 48px; + height: 48px; + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 5px solid rgba(255, 227, 0, 255); + border-bottom-color: transparent; + border-radius: 50%; + animation: rotation 1s linear 1s infinite; + visibility: hidden; + margin: auto; + } + + @keyframes rotation { + 0% { + visibility: visible; + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +""".trimIndent() + +external fun onWasmReady(onReady: () -> Unit) + +fun appyxSample( + block: () -> Unit, +) { + appendLoaderStyles() + appendLoaderElement() + onWasmReady { + block() + removeLoaderElement() + } +} + +private fun appendLoaderStyles() { + val head = document.head ?: document.getElementsByTagName("head")[0] + head?.apply { + val style = document.createElement("style") + head.appendChild(style) + style.appendChild(document.createTextNode(LOADER_STYLES)) + } +} + +private fun appendLoaderElement() { + val composeTarget = document.getElementById("ComposeTarget") + val loader = document.createElement("div") + loader.className = "loader" + composeTarget?.parentNode?.appendChild(loader) +} + +private fun removeLoaderElement() { + val loader = document.getElementsByClassName("loader")[0] + loader?.parentNode?.removeChild(loader) +} \ No newline at end of file diff --git a/demos/sandbox-appyx-navigation/common/build.gradle.kts b/demos/sandbox-appyx-navigation/common/build.gradle.kts index 5762b9f39..3ed22313b 100644 --- a/demos/sandbox-appyx-navigation/common/build.gradle.kts +++ b/demos/sandbox-appyx-navigation/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -23,6 +25,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "demo-sandbox-appyx-navigation-common" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "demo-sandbox-appyx-navigation-common-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() iosArm64() @@ -88,11 +106,16 @@ android { } } +compose.experimental { + web.application {} +} + dependencies { add("kspCommonMainMetadata", project(":ksp:appyx-processor")) add("kspAndroid", project(":ksp:appyx-processor")) add("kspDesktop", project(":ksp:appyx-processor")) add("kspJs", project(":ksp:appyx-processor")) + add("kspWasmJs", project(":ksp:appyx-processor")) add("kspIosArm64", project(":ksp:appyx-processor")) add("kspIosX64", project(":ksp:appyx-processor")) add("kspIosSimulatorArm64", project(":ksp:appyx-processor")) diff --git a/demos/sandbox-appyx-navigation/common/karma.config.d/wasm/config.js b/demos/sandbox-appyx-navigation/common/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/sandbox-appyx-navigation/common/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/sandbox-appyx-navigation/web/build.gradle.kts b/demos/sandbox-appyx-navigation/web/build.gradle.kts index 6d7ea56ed..f946ce471 100644 --- a/demos/sandbox-appyx-navigation/web/build.gradle.kts +++ b/demos/sandbox-appyx-navigation/web/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("org.jetbrains.compose") @@ -9,6 +11,20 @@ kotlin { browser() binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "appyx-demos-sandbox-navigation-web-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() + } sourceSets { val commonMain by getting { dependencies { diff --git a/demos/sandbox-appyx-navigation/web/karma.config.d/wasm/config.js b/demos/sandbox-appyx-navigation/web/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/demos/sandbox-appyx-navigation/web/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/demos/sandbox-appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sandbox/navigation/Main.kt b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sandbox/navigation/Main.kt new file mode 100644 index 000000000..19753aae1 --- /dev/null +++ b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sandbox/navigation/Main.kt @@ -0,0 +1,96 @@ +package com.bumble.appyx.demos.sandbox.navigation + +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.CanvasBasedWindow +import com.bumble.appyx.demos.sandbox.navigation.node.container.MainNavNode +import com.bumble.appyx.demos.sandbox.navigation.ui.AppyxSampleAppTheme +import com.bumble.appyx.navigation.integration.ScreenSize +import com.bumble.appyx.navigation.integration.WebNodeHost +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +external fun onWasmReady(onReady: () -> Unit) + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + val events: Channel = Channel() + onWasmReady { + CanvasBasedWindow("Navigation Demo") { + val requester = remember { FocusRequester() } + var hasFocus by remember { mutableStateOf(false) } + + var screenSize by remember { mutableStateOf(ScreenSize(0.dp, 0.dp)) } + val eventScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) } + + AppyxSampleAppTheme { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier + .fillMaxSize() + .onSizeChanged { screenSize = ScreenSize(it.width.dp, it.height.dp) } + .onKeyEvent { + onKeyEvent(it, events, eventScope) + } + .focusRequester(requester) + .focusable() + .onFocusChanged { hasFocus = it.hasFocus } + ) { + WebNodeHost( + screenSize = screenSize, + onBackPressedEvents = events.receiveAsFlow(), + ) { nodeContext -> + MainNavNode( + nodeContext = nodeContext, + ) + } + } + + if (!hasFocus) { + LaunchedEffect(Unit) { + requester.requestFocus() + } + } + } + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +private fun onKeyEvent( + keyEvent: KeyEvent, + events: Channel, + coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main), +): Boolean = + when { + keyEvent.type == KeyEventType.KeyUp && keyEvent.key == Key.Backspace -> { + coroutineScope.launch { events.send(Unit) } + true + } + + else -> false + } diff --git a/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/index.html b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/index.html new file mode 100644 index 000000000..b09444ce6 --- /dev/null +++ b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + Navigation Demo + + + + +
+ +
+ + + diff --git a/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/styles.css b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/styles.css new file mode 100644 index 000000000..8655f2e76 --- /dev/null +++ b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/styles.css @@ -0,0 +1,12 @@ +#root { + width: 100%; + height: 100vh; +} + +body { + margin: 0; +} + +#root > .compose-web-column > div { + position: relative; +} diff --git a/documentation/navigation/multiplatform.md b/documentation/navigation/multiplatform.md index 2cf2e5d1d..e23501fe1 100644 --- a/documentation/navigation/multiplatform.md +++ b/documentation/navigation/multiplatform.md @@ -138,7 +138,7 @@ fun main() = application { fun main() { val events: Channel = Channel() onWasmReady { - CanvasBasedWindow("Your app") { + CanvasBasedWindow("Your app") { val requester = remember { FocusRequester() } var hasFocus by remember { mutableStateOf(false) } var screenSize by remember { mutableStateOf(ScreenSize(0.dp, 0.dp)) } diff --git a/gradle.properties b/gradle.properties index 1c621a29a..1bd124c1e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,5 @@ org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 org.gradle.parallel=true org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.uikit.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6093a067b..daf172cc1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ kotlin = "1.9.23" ksp = "1.9.23-1.0.19" mvicore = "1.2.6" ribs = "0.39.0" -serialization-json = "1.5.0" +serialization-json = "1.6.2" uuid = "9.0.0" uiautomator = "2.2.0" benchmark-macro-junit4 = "1.2.0" diff --git a/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt b/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt index a29a7306f..04f47b8aa 100644 --- a/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt +++ b/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt @@ -12,6 +12,7 @@ import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.getByType import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.kotlinExtension import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl class MultiplatformConventionPlugin : Plugin { @@ -34,6 +35,8 @@ class MultiplatformConventionPlugin : Plugin { "src/desktopTest/kotlin", "src/jsMain/kotlin", "src/jsTest/kotlin", + "src/wasmJsMain/kotlin", + "src/wasmJsTest/kotlin", "src/iosMain/kotlin", ) } diff --git a/utils/customisations/build.gradle.kts b/utils/customisations/build.gradle.kts index 6112939c7..6270c521d 100644 --- a/utils/customisations/build.gradle.kts +++ b/utils/customisations/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("com.android.library") @@ -21,6 +23,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-utils-customisation" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-utils-customisation-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() iosArm64() @@ -40,4 +58,4 @@ kotlin { iosSimulatorArm64Main.dependsOn(this) } } -} +} \ No newline at end of file diff --git a/utils/customisations/karma.config.d/wasm/config.js b/utils/customisations/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/utils/customisations/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/utils/customisations/src/wasmJsMain/kotlin/com/bumble/appyx/utils/customisations/NodeCustomisationDirectoryImpl.kt b/utils/customisations/src/wasmJsMain/kotlin/com/bumble/appyx/utils/customisations/NodeCustomisationDirectoryImpl.kt new file mode 100644 index 000000000..7e369f541 --- /dev/null +++ b/utils/customisations/src/wasmJsMain/kotlin/com/bumble/appyx/utils/customisations/NodeCustomisationDirectoryImpl.kt @@ -0,0 +1,33 @@ +package com.bumble.appyx.utils.customisations + +import kotlin.reflect.KClass + +actual open class NodeCustomisationDirectoryImpl actual constructor( + override val parent: NodeCustomisationDirectory? +) : MutableNodeCustomisationDirectory { + + override fun put(key: KClass, valueProvider: () -> T) { + // NO-OP + } + + override fun get(key: KClass): T? = + null + + override fun getRecursively(key: KClass): T? = + null + + override fun putSubDirectory(key: KClass, valueProvider: () -> NodeCustomisationDirectory) { + // NO-OP + } + + override fun getSubDirectory(key: KClass): NodeCustomisationDirectory? = + null + + override fun getSubDirectoryOrSelf(key: KClass): NodeCustomisationDirectory { + return this + } + + operator fun KClass<*>.invoke(block: NodeCustomisationDirectoryImpl.() -> Unit) { + // NO-OP + } +} diff --git a/utils/material3/build.gradle.kts b/utils/material3/build.gradle.kts index 2a7d0914d..0b77ea233 100644 --- a/utils/material3/build.gradle.kts +++ b/utils/material3/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") id("com.android.library") @@ -22,8 +24,23 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-utils-material3" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-utils-material3-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } - iosX64() iosArm64() iosSimulatorArm64() @@ -52,3 +69,7 @@ kotlin { } } } + +compose.experimental { + web.application {} +} diff --git a/utils/material3/karma.config.d/wasm/config.js b/utils/material3/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/utils/material3/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/utils/multiplatform/build.gradle.kts b/utils/multiplatform/build.gradle.kts index eed86cfd3..22938fbd7 100644 --- a/utils/multiplatform/build.gradle.kts +++ b/utils/multiplatform/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { id("com.bumble.appyx.multiplatform") kotlin("plugin.serialization") @@ -23,6 +25,22 @@ kotlin { // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 moduleName = "appyx-utils-multiplatform" browser() + binaries.executable() + } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 + moduleName = "appyx-utils-multiplatform-wa" + browser { + // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223 + testTask { + useKarma { + useChromeHeadless() + useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm")) + } + } + } + binaries.executable() } iosX64() iosArm64() diff --git a/utils/multiplatform/karma.config.d/wasm/config.js b/utils/multiplatform/karma.config.d/wasm/config.js new file mode 100644 index 000000000..22429e585 --- /dev/null +++ b/utils/multiplatform/karma.config.d/wasm/config.js @@ -0,0 +1,55 @@ +// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file +// This file provides karma.config.d configuration to run tests with k/wasm + +const path = require("path"); + +config.browserConsoleLogOptions.level = "debug"; + +const basePath = config.basePath; +const projectPath = path.resolve(basePath, "..", "..", "..", ".."); +const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out") + +const debug = message => console.log(`[karma-config] ${message}`); + +debug(`karma basePath: ${basePath}`); +debug(`karma generatedAssetsPath: ${generatedAssetsPath}`); + +config.proxies["/"] = path.resolve(basePath, "kotlin"); + +config.files = [ + {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false}, + {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false}, +].concat(config.files); + +function KarmaWebpackOutputFramework(config) { + // This controller is instantiated and set during the preprocessor phase. + const controller = config.__karmaWebpackController; + + // only if webpack has instantiated its controller + if (!controller) { + console.warn( + "Webpack has not instantiated controller yet.\n" + + "Check if you have enabled webpack preprocessor and framework before this framework" + ) + return + } + + config.files.push({ + pattern: `${controller.outputPath}/**/*`, + included: false, + served: true, + watched: false + }) +} + +const KarmaWebpackOutputPlugin = { + 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], +}; + +config.plugins.push(KarmaWebpackOutputPlugin); +config.frameworks.push("webpack-output"); \ No newline at end of file diff --git a/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/AppyxLogger.wasmJs.kt b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/AppyxLogger.wasmJs.kt new file mode 100644 index 000000000..3fa4a92a8 --- /dev/null +++ b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/AppyxLogger.wasmJs.kt @@ -0,0 +1,50 @@ +package com.bumble.appyx.utils.multiplatform + +import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.DEBUG +import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.DISABLED +import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.ERROR +import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.INFO +import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.VERBOSE +import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.WARN + +external interface Console { + fun log(o: String) +} + +external val console: Console + +actual object AppyxLogger { + + actual var loggingLevel: Int = DISABLED + + actual fun v(tag: String, message: String) { + if (loggingLevel <= VERBOSE) { + console.log("$tag: $message") + } + } + + actual fun d(tag: String, message: String) { + if (loggingLevel <= DEBUG) { + console.log("$tag: $message") + } + } + + + actual fun i(tag: String, message: String) { + if (loggingLevel <= INFO) { + console.log("$tag: $message") + } + } + + actual fun w(tag: String, message: String) { + if (loggingLevel <= WARN) { + console.log("$tag: $message") + } + } + + actual fun e(tag: String, message: String) { + if (loggingLevel <= ERROR) { + console.log("$tag: $message") + } + } +} diff --git a/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/BuildFlags.kt b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/BuildFlags.kt new file mode 100644 index 000000000..133ce0315 --- /dev/null +++ b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/BuildFlags.kt @@ -0,0 +1,6 @@ +package com.bumble.appyx.utils.multiplatform + +@Suppress("ForbiddenComment") +actual object BuildFlags { + actual val DEBUG: Boolean = false // TODO: provide value from gradle +} diff --git a/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/Parcelable.kt b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/Parcelable.kt new file mode 100644 index 000000000..eef475202 --- /dev/null +++ b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/Parcelable.kt @@ -0,0 +1,8 @@ +package com.bumble.appyx.utils.multiplatform + +actual annotation class Parcelize + +actual interface Parcelable + +@Target(AnnotationTarget.TYPE) +actual annotation class RawValue