diff --git a/examples/falling-balls-mpp/build.gradle.kts b/examples/falling-balls-mpp/build.gradle.kts new file mode 100644 index 0000000000..44861b3c33 --- /dev/null +++ b/examples/falling-balls-mpp/build.gradle.kts @@ -0,0 +1,243 @@ +import org.jetbrains.compose.compose +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension + +buildscript { + repositories { + mavenLocal() + mavenCentral() + google() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } +} + +plugins { + kotlin("multiplatform") version "1.6.10" + id("org.jetbrains.compose") version "1.1.0-beta04" +} + +version = "1.0-SNAPSHOT" + +val resourcesDir = "$buildDir/resources" +val skikoWasm by configurations.creating + +dependencies { + skikoWasm("org.jetbrains.skiko:skiko-js-wasm-runtime:0.6.9") +} + +val unzipTask = tasks.register("unzipWasm", Copy::class) { + destinationDir = file(resourcesDir) + from(skikoWasm.map { zipTree(it) }) +} + +repositories { + mavenLocal() + mavenCentral() + google() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +} + +kotlin { + jvm("desktop") + js(IR) { + browser() + binaries.executable() + } + macosX64 { + binaries { + executable { + entryPoint = "main" + freeCompilerArgs += listOf( + "-linker-option", "-framework", "-linker-option", "Metal" + ) + } + } + } + macosArm64 { + binaries { + executable { + entryPoint = "main" + freeCompilerArgs += listOf( + "-linker-option", "-framework", "-linker-option", "Metal" + ) + } + } + } + iosX64("uikitX64") { + binaries { + executable() { + entryPoint = "main" + freeCompilerArgs += listOf( + "-linker-option", "-framework", "-linker-option", "Metal", + "-linker-option", "-framework", "-linker-option", "CoreText", + "-linker-option", "-framework", "-linker-option", "CoreGraphics" + ) + // TODO: the current compose binary surprises LLVM, so disable checks for now. + freeCompilerArgs += "-Xdisable-phases=VerifyBitcode" + } + } + } + iosArm64("uikitArm64") { + binaries { + executable() { + entryPoint = "main" + freeCompilerArgs += listOf( + "-linker-option", "-framework", "-linker-option", "Metal", + "-linker-option", "-framework", "-linker-option", "CoreText", + "-linker-option", "-framework", "-linker-option", "CoreGraphics" + ) + // TODO: the current compose binary surprises LLVM, so disable checks for now. + freeCompilerArgs += "-Xdisable-phases=VerifyBitcode" + } + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.runtime) + implementation("org.jetbrains.skiko:skiko:0.6.7") + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + + val desktopMain by getting { + dependencies { + implementation(compose.desktop.currentOs) + } + } + + val jsMain by getting { + resources.setSrcDirs(resources.srcDirs) + resources.srcDirs(unzipTask.map { it.destinationDir }) + } + + val nativeMain by creating { + dependsOn(commonMain) + } + val macosMain by creating { + dependsOn(nativeMain) + } + val macosX64Main by getting { + dependsOn(macosMain) + } + val macosArm64Main by getting { + dependsOn(macosMain) + } + val uikitMain by creating { + dependsOn(nativeMain) + } + val uikitX64Main by getting { + dependsOn(uikitMain) + } + val uikitArm64Main by getting { + dependsOn(uikitMain) + } + } +} + +compose.desktop { + application { + mainClass = "Main_desktopKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "Falling Balls" + packageVersion = "1.0.0" + + windows { + menuGroup = "Compose Examples" + // see https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html + upgradeUuid = "18159995-d967-4CD2-8885-77BFA97CFA9F" + } + } + } +} + +tasks.withType { + kotlinOptions.jvmTarget = "11" +} + +kotlin { + targets.withType { + binaries.all { + freeCompilerArgs += "-Xdisable-phases=VerifyBitcode" + } + } +} + +// a temporary workaround for a bug in jsRun invocation - see https://youtrack.jetbrains.com/issue/KT-48273 +afterEvaluate { + rootProject.extensions.configure { + versions.webpackDevServer.version = "4.0.0" + versions.webpackCli.version = "4.9.0" + nodeVersion = "16.0.0" + } +} + +enum class Target(val simulator: Boolean, val key: String) { + UIKIT_X64(true, "uikitX64"), UIKIT_ARM64(false, "uikitArm64") +} + +if (System.getProperty("os.name") == "Mac OS X") { +// Create Xcode integration tasks. + val sdkName: String? = System.getenv("SDK_NAME") + + val target = sdkName.orEmpty().let { + when { + it.startsWith("iphoneos") -> Target.UIKIT_ARM64 + it.startsWith("iphonesimulator") -> Target.UIKIT_X64 + else -> Target.UIKIT_X64 + } + } + + val targetBuildDir: String? = System.getenv("TARGET_BUILD_DIR") + val executablePath: String? = System.getenv("EXECUTABLE_PATH") + val buildType = System.getenv("CONFIGURATION")?.let { + org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.valueOf(it.toUpperCase()) + } ?: org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG + + val currentTarget = kotlin.targets[target.key] as org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + val kotlinBinary = currentTarget.binaries.getExecutable(buildType) + val xcodeIntegrationGroup = "Xcode integration" + + val packForXCode = if (sdkName == null || targetBuildDir == null || executablePath == null) { + // The build is launched not by Xcode -> + // We cannot create a copy task and just show a meaningful error message. + tasks.create("packForXCode").doLast { + throw IllegalStateException("Please run the task from Xcode") + } + } else { + // Otherwise copy the executable into the Xcode output directory. + tasks.create("packForXCode", Copy::class.java) { + dependsOn(kotlinBinary.linkTask) + + destinationDir = file(targetBuildDir) + + val dsymSource = kotlinBinary.outputFile.absolutePath + ".dSYM" + val dsymDestination = File(executablePath).parentFile.name + ".dSYM" + val oldExecName = kotlinBinary.outputFile.name + val newExecName = File(executablePath).name + + from(dsymSource) { + into(dsymDestination) + rename(oldExecName, newExecName) + } + + from(kotlinBinary.outputFile) { + rename { executablePath } + } + } + } +} diff --git a/examples/falling-balls-mpp/gradle.properties b/examples/falling-balls-mpp/gradle.properties new file mode 100644 index 0000000000..05c36706e3 --- /dev/null +++ b/examples/falling-balls-mpp/gradle.properties @@ -0,0 +1,5 @@ +kotlin.native.cacheKind=none +kotlin.native.useEmbeddableCompilerJar=true +org.gradle.jvmargs=-Xmx3g +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false diff --git a/examples/falling-balls-mpp/gradle/wrapper/gradle-wrapper.jar b/examples/falling-balls-mpp/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..e708b1c023 Binary files /dev/null and b/examples/falling-balls-mpp/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/falling-balls-mpp/gradle/wrapper/gradle-wrapper.properties b/examples/falling-balls-mpp/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..05679dc3c1 --- /dev/null +++ b/examples/falling-balls-mpp/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/falling-balls-mpp/gradlew b/examples/falling-balls-mpp/gradlew new file mode 100755 index 0000000000..4f906e0c81 --- /dev/null +++ b/examples/falling-balls-mpp/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/examples/falling-balls-mpp/project.yml b/examples/falling-balls-mpp/project.yml new file mode 100644 index 0000000000..ad2ce6cda9 --- /dev/null +++ b/examples/falling-balls-mpp/project.yml @@ -0,0 +1,29 @@ +name: ComposeFallingBalls +options: + bundleIdPrefix: org.jetbrains +settings: + DEVELOPMENT_TEAM: N462MKSJ7M + CODE_SIGN_IDENTITY: "iPhone Developer" + CODE_SIGN_STYLE: Automatic + MARKETING_VERSION: "1.0" + CURRENT_PROJECT_VERSION: "4" + SDKROOT: iphoneos +targets: + ComposeFallingBalls: + type: application + platform: iOS + deploymentTarget: "12.0" + prebuildScripts: + - script: cd "$SRCROOT" && ./gradlew -i -p . packForXCode + name: GradleCompile + info: + path: plists/Ios/Info.plist + properties: + UILaunchStoryboardName: "" + sources: + - "src/" + settings: + LIBRARY_SEARCH_PATHS: "$(inherited)" + ENABLE_BITCODE: "YES" + ONLY_ACTIVE_ARCH: "NO" + VALID_ARCHS: "arm64" diff --git a/examples/falling-balls-mpp/settings.gradle.kts b/examples/falling-balls-mpp/settings.gradle.kts new file mode 100644 index 0000000000..1c3a625f81 --- /dev/null +++ b/examples/falling-balls-mpp/settings.gradle.kts @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } + google() + } + +} +rootProject.name = "falling-balls-mpp" + diff --git a/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/FallingBalls.kt b/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/FallingBalls.kt new file mode 100644 index 0000000000..716dd0a725 --- /dev/null +++ b/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/FallingBalls.kt @@ -0,0 +1,92 @@ +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.Slider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import org.jetbrains.skiko.KotlinBackend + +@Composable +fun FallingBalls(game: Game) { + val density = LocalDensity.current + Column { + Text( + "Catch balls!${if (game.finished) " Game over!" else ""}", + fontSize = 1.8f.em, + color = Color(218, 120, 91) + ) + Text("Score: ${game.score} Time: ${game.elapsed / 1_000_000} Blocks: ${game.numBlocks}", fontSize = 1.8f.em) + Row { + if (!game.started) { + Slider( + value = game.numBlocks / 20f, + onValueChange = { game.numBlocks = (it * 20f).toInt().coerceAtLeast(1) }, + modifier = Modifier.width(100.dp) + ) + } + Button( + modifier = Modifier + .border(2.dp, Color(255, 215, 0)) + .background(Color.Yellow), + onClick = { + game.started = !game.started + if (game.started) { + game.start() + } + } + ) { + Text(if (game.started) "Stop" else "Start", fontSize = 2f.em) + } + if (game.started) { + Button( + modifier = Modifier + .offset(10.dp, 0.dp) + .border(2.dp, Color(255, 215, 0)) + .background(Color.Yellow), + onClick = { + game.togglePause() + }) { + Text(if (game.paused) "Resume" else "Pause", fontSize = 2f.em) + } + } + } + if (game.started) { + Box(modifier = Modifier.height(20.dp)) + Box(modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(1f) + .onSizeChanged { + with(density) { + if (org.jetbrains.skiko.kotlinBackend == KotlinBackend.JVM) { + // Really ugly hack, until we properly pass geometry. + // TODO: fix me in Compose. + game.width = it.width.toDp() + game.height = it.height.toDp() + } + } + } + ) { + game.pieces.forEachIndexed { index, piece -> Piece(index, piece) } + } + } + + LaunchedEffect(Unit) { + while (true) { + withFrameNanos { + if (game.started && !game.paused && !game.finished) + game.update(it) + } + } + } + } +} + diff --git a/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/Game.kt b/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/Game.kt new file mode 100644 index 0000000000..8c65b92abe --- /dev/null +++ b/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/Game.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.* +import kotlin.random.Random + +interface Time { + fun now(): Long +} + +class Game(val time: Time) { + private var previousTimeNanos: Long = Long.MAX_VALUE + private val colors = arrayOf( + Color.Red, Color.Blue, Color.Cyan, + Color.Magenta, Color.Yellow, Color.Black + ) + private var startTime = 0L + + var width by mutableStateOf(0.dp) + var height by mutableStateOf(0.dp) + + var pieces = mutableStateListOf() + private set + + var elapsed by mutableStateOf(0L) + var score by mutableStateOf(0) + private var clicked by mutableStateOf(0) + + var started by mutableStateOf(false) + var paused by mutableStateOf(false) + var finished by mutableStateOf(false) + + var numBlocks by mutableStateOf(5) + + fun start() { + previousTimeNanos = time.now() + startTime = previousTimeNanos + clicked = 0 + started = true + finished = false + paused = false + pieces.clear() + repeat(numBlocks) { index -> + pieces.add(PieceData(this, index * 1.5f + 5f, colors[index % colors.size]).also { piece -> + piece.position = Random.nextDouble(0.0, 100.0).toFloat() + }) + } + } + + fun togglePause() { + paused = !paused + previousTimeNanos = time.now() + } + + fun update(nanos: Long) { + val dt = (nanos - previousTimeNanos).coerceAtLeast(0) + previousTimeNanos = nanos + elapsed = nanos - startTime + pieces.forEach { it.update(dt) } + } + + fun clicked(piece: PieceData) { + score += piece.velocity.toInt() + clicked++ + if (clicked == numBlocks) { + finished = true + } + } +} \ No newline at end of file diff --git a/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/Piece.kt b/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/Piece.kt new file mode 100644 index 0000000000..c155ba8947 --- /dev/null +++ b/examples/falling-balls-mpp/src/commonMain/kotlin/fallingBalls/Piece.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +internal fun Piece(index: Int, piece: PieceData) { + val boxSize = 40.dp + Box( + Modifier + .offset(boxSize * index * 5 / 3, piece.position.dp) + .shadow(30.dp) + .clip(CircleShape) + ) { + Box( + Modifier + .size(boxSize, boxSize) + .background(if (piece.clicked) Color.Gray else piece.color) + .clickable(onClick = { piece.click() }) + ) + } +} + +data class PieceData(val game: Game, val velocity: Float, val color: Color) { + var clicked by mutableStateOf(false) + var position by mutableStateOf(0f) + + fun update(dt: Long) { + if (clicked) return + val delta = (dt / 1E8 * velocity).toFloat() + position = if (position < game.height.value) position + delta else 0f + } + + fun click() { + if (!clicked && !game.paused) { + clicked = true + game.clicked(this) + } + } +} \ No newline at end of file diff --git a/examples/falling-balls-mpp/src/desktopMain/kotlin/main.desktop.kt b/examples/falling-balls-mpp/src/desktopMain/kotlin/main.desktop.kt new file mode 100644 index 0000000000..e4fe019def --- /dev/null +++ b/examples/falling-balls-mpp/src/desktopMain/kotlin/main.desktop.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication + +object JvmTime : Time { + override fun now(): Long = System.nanoTime() +} + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = + singleWindowApplication( + title = "Falling Balls", + state = WindowState(size = DpSize(800.dp, 800.dp)) + ) { + val game = remember { Game(JvmTime) } + FallingBalls(game) + } + +@Preview +@Composable +fun GamePreview() { + val game = remember { Game(JvmTime) } + FallingBalls(game) +} \ No newline at end of file diff --git a/examples/falling-balls-mpp/src/jsMain/kotlin/main.js.kt b/examples/falling-balls-mpp/src/jsMain/kotlin/main.js.kt new file mode 100644 index 0000000000..811ece5aa1 --- /dev/null +++ b/examples/falling-balls-mpp/src/jsMain/kotlin/main.js.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import org.jetbrains.skiko.wasm.onWasmReady + +object JsTime : Time { + override fun now(): Long = kotlinx.browser.window.performance.now().toLong() +} + +fun main() { + onWasmReady { + Window("Falling Balls") { + val game = remember { Game(JsTime).apply { + // Ugly hack, properly pass geometry. + width = 800.dp + height = 800.dp + }} + FallingBalls(game) + } + } +} + diff --git a/examples/falling-balls-mpp/src/jsMain/resources/index.html b/examples/falling-balls-mpp/src/jsMain/resources/index.html new file mode 100644 index 0000000000..93e55ae53d --- /dev/null +++ b/examples/falling-balls-mpp/src/jsMain/resources/index.html @@ -0,0 +1,32 @@ + + + + + + + compose multiplatform web demo + + + + +

compose multiplatform web demo

+
+ +
+ + + diff --git a/examples/falling-balls-mpp/src/jsMain/resources/styles.css b/examples/falling-balls-mpp/src/jsMain/resources/styles.css new file mode 100644 index 0000000000..e5b3293a7a --- /dev/null +++ b/examples/falling-balls-mpp/src/jsMain/resources/styles.css @@ -0,0 +1,8 @@ +#root { + width: 100%; + height: 100vh; +} + +#root > .compose-web-column > div { + position: relative; +} \ No newline at end of file diff --git a/examples/falling-balls-mpp/src/macosMain/kotlin/main.macos.kt b/examples/falling-balls-mpp/src/macosMain/kotlin/main.macos.kt new file mode 100644 index 0000000000..a8fd142c92 --- /dev/null +++ b/examples/falling-balls-mpp/src/macosMain/kotlin/main.macos.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.ui.window.Window +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.dp +import platform.AppKit.NSApp +import platform.AppKit.NSApplication + +object MacosTime : Time { + override fun now(): Long = kotlin.system.getTimeNanos() +} + +fun main() { + NSApplication.sharedApplication() + Window("Falling Balls") { + val game = remember { Game(MacosTime).apply { + // TODO: rework, now we do not properly propagate geometry changes. + width = 800.dp + height = 600.dp + } } + FallingBalls(game) + } + NSApp?.run() +} diff --git a/examples/falling-balls-mpp/src/uikitMain/kotlin/main.uikit.kt b/examples/falling-balls-mpp/src/uikitMain/kotlin/main.uikit.kt new file mode 100644 index 0000000000..082b41477c --- /dev/null +++ b/examples/falling-balls-mpp/src/uikitMain/kotlin/main.uikit.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +// Use `xcodegen` first, then `open ./SkikoSample.xcodeproj` and then Run button in XCode. +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Application +import kotlinx.cinterop.* +import platform.UIKit.* +import platform.Foundation.* + +object UikitTime : Time { + override fun now(): Long = kotlin.system.getTimeNanos() +} + +fun main() { + val args = emptyArray() + memScoped { + val argc = args.size + 1 + val argv = (arrayOf("skikoApp") + args).map { it.cstr.ptr }.toCValues() + autoreleasepool { + UIApplicationMain(argc, argv, null, NSStringFromClass(SkikoAppDelegate)) + } + } +} + +class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol { + companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta + + @ObjCObjectBase.OverrideInit + constructor() : super() + + private var _window: UIWindow? = null + override fun window() = _window + override fun setWindow(window: UIWindow?) { + _window = window + } + + override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map?): Boolean { + window = UIWindow(frame = UIScreen.mainScreen.bounds) + window!!.rootViewController = Application("Falling Balls") { + val game = remember { Game(UikitTime).apply { + width = 800.dp + height = 800.dp + } } + Column { + // To skip upper part of screen. + Box(modifier = Modifier + .height(100.dp)) + FallingBalls(game) + } + } + window!!.makeKeyAndVisible() + return true + } +}