Browse Source

LWJGL integration example (#1275)

pull/1277/head
Igor Demin 3 years ago committed by GitHub
parent
commit
9806bdc085
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 15
      experimental/lwjgl-integration/.gitignore
  3. 21
      experimental/lwjgl-integration/.run/desktop.run.xml
  4. 7
      experimental/lwjgl-integration/README.md
  5. 46
      experimental/lwjgl-integration/build.gradle.kts
  6. 2
      experimental/lwjgl-integration/gradle.properties
  7. BIN
      experimental/lwjgl-integration/gradle/wrapper/gradle-wrapper.jar
  8. 5
      experimental/lwjgl-integration/gradle/wrapper/gradle-wrapper.properties
  9. 183
      experimental/lwjgl-integration/gradlew
  10. 100
      experimental/lwjgl-integration/gradlew.bat
  11. 6
      experimental/lwjgl-integration/settings.gradle.kts
  12. 43
      experimental/lwjgl-integration/src/main/kotlin/App.kt
  13. 36
      experimental/lwjgl-integration/src/main/kotlin/GlfwCoroutineDispatcher.kt
  14. 124
      experimental/lwjgl-integration/src/main/kotlin/GlfwEvents.kt
  15. 113
      experimental/lwjgl-integration/src/main/kotlin/GlfwKeyMapping.kt
  16. 86
      experimental/lwjgl-integration/src/main/kotlin/main.kt

2
README.md

@ -47,6 +47,8 @@ Preview functionality (check your application UI without building/running it) fo
* [components](components) - custom components of Compose Multiplatform * [components](components) - custom components of Compose Multiplatform
* [Video Player](components/VideoPlayer) * [Video Player](components/VideoPlayer)
* [Split Pane](components/SplitPane) * [Split Pane](components/SplitPane)
* [experimental](experimental) - experimental components and examples
* [LWJGL integration](experimental/lwjgl-integration) - An example showing how to integrate Compose with [LWJGL](https://www.lwjgl.org)
## Getting latest version of Compose Multiplatform ## ## Getting latest version of Compose Multiplatform ##

15
experimental/lwjgl-integration/.gitignore vendored

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build/
/captures
.externalNativeBuild
.cxx

21
experimental/lwjgl-integration/.run/desktop.run.xml

@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

7
experimental/lwjgl-integration/README.md

@ -0,0 +1,7 @@
An example showing how to integrate Compose with [LWJGL](https://www.lwjgl.org)
Note that:
- the integration is very experimental and can be unstable
- not all features are implemented
- not all features are currently supported (Accessibility, Input Methods)
- to pass some event information it is needed to pass it via AWT events (java.awt.KeyEvent and java.awt.MouseEvent). In the future versions of Compose we plan to get rid of the need of AWT events.

46
experimental/lwjgl-integration/build.gradle.kts

@ -0,0 +1,46 @@
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
// __KOTLIN_COMPOSE_VERSION__
kotlin("jvm") version "1.5.31"
// __LATEST_COMPOSE_RELEASE_VERSION__
id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "1.0.0-alpha4-build411")
}
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}
val osName: String = System.getProperty("os.name")
val os = when {
osName == "Mac OS X" -> "macos"
osName == "Linux" -> "linux"
osName.startsWith("Win") -> "windows"
else -> throw Error("Unknown OS $osName")
}
dependencies {
implementation(compose.desktop.currentOs)
implementation("org.lwjgl:lwjgl:3.2.3")
implementation("org.lwjgl:lwjgl-glfw:3.2.3")
implementation("org.lwjgl:lwjgl-opengl:3.2.3")
implementation("org.lwjgl:lwjgl:3.2.3:natives-$os")
implementation("org.lwjgl:lwjgl-glfw:3.2.3:natives-$os")
implementation("org.lwjgl:lwjgl-opengl:3.2.3:natives-$os")
}
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
appResourcesRootDir.set(project.layout.projectDirectory.dir("xxx"))
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "KotlinJvmComposeDesktopApplication"
packageVersion = "1.0.0"
}
}
}

2
experimental/lwjgl-integration/gradle.properties

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
kotlin.code.style=official

BIN
experimental/lwjgl-integration/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

5
experimental/lwjgl-integration/gradle/wrapper/gradle-wrapper.properties vendored

@ -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

183
experimental/lwjgl-integration/gradlew vendored

@ -0,0 +1,183 @@
#!/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" "$@"

100
experimental/lwjgl-integration/gradlew.bat vendored

@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

6
experimental/lwjgl-integration/settings.gradle.kts

@ -0,0 +1,6 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}

43
experimental/lwjgl-integration/src/main/kotlin/App.kt

@ -0,0 +1,43 @@
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun App() {
Column {
var text by remember { mutableStateOf("Text") }
TextField(text, { text = it })
Button({}) {
Text("Hello!")
}
Box(Modifier.weight(1f)) {
val state = rememberLazyListState()
LazyColumn(state = state, modifier = Modifier.width(200.dp).fillMaxHeight()) {
items(100) {
Text("Item $it")
}
}
VerticalScrollbar(
rememberScrollbarAdapter(state),
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight()
)
}
}
}

36
experimental/lwjgl-integration/src/main/kotlin/GlfwCoroutineDispatcher.kt

@ -0,0 +1,36 @@
import kotlinx.coroutines.CoroutineDispatcher
import org.lwjgl.glfw.GLFW
import kotlin.coroutines.CoroutineContext
class GlfwCoroutineDispatcher : CoroutineDispatcher() {
private val tasks = mutableListOf<Runnable>()
private val tasksCopy = mutableListOf<Runnable>()
private var isStopped = false
fun runLoop() {
while (!isStopped) {
synchronized(tasks) {
tasksCopy.addAll(tasks)
tasks.clear()
}
for (runnable in tasksCopy) {
if (!isStopped) {
runnable.run()
}
}
tasksCopy.clear()
GLFW.glfwWaitEvents()
}
}
fun stop() {
isStopped = true
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
synchronized(tasks) {
tasks.add(block)
}
GLFW.glfwPostEmptyEvent()
}
}

124
experimental/lwjgl-integration/src/main/kotlin/GlfwEvents.kt

@ -0,0 +1,124 @@
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.NativeKeyEvent
import androidx.compose.ui.input.mouse.MouseScrollOrientation
import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.unit.Density
import org.lwjgl.glfw.GLFW.*
import java.awt.Component
import java.awt.event.InputEvent
import java.awt.event.MouseEvent
@OptIn(ExperimentalComposeUiApi::class)
fun ComposeScene.subscribeToGLFWEvents(windowHandle: Long) {
glfwSetMouseButtonCallback(windowHandle) { _, button, action, mods ->
when (action) {
GLFW_PRESS -> PointerEventType.Press
GLFW_RELEASE -> PointerEventType.Release
else -> PointerEventType.Unknown
}
sendPointerEvent(
position = glfwGetCursorPos(windowHandle),
eventType = when (action) {
GLFW_PRESS -> PointerEventType.Press
GLFW_RELEASE -> PointerEventType.Release
else -> PointerEventType.Unknown
},
mouseEvent = MouseEvent(getAwtMods(windowHandle))
)
}
glfwSetCursorPosCallback(windowHandle) { _, xpos, ypos ->
sendPointerEvent(
position = Offset(xpos.toFloat(), ypos.toFloat()),
eventType = PointerEventType.Move,
mouseEvent = MouseEvent(getAwtMods(windowHandle))
)
}
glfwSetCursorEnterCallback(windowHandle) { _, entered ->
sendPointerEvent(
position = glfwGetCursorPos(windowHandle),
eventType = if (entered) PointerEventType.Enter else PointerEventType.Exit,
mouseEvent = MouseEvent(getAwtMods(windowHandle))
)
}
glfwSetScrollCallback(windowHandle) { _, xoffset, yoffset ->
sendPointerScrollEvent(
position = glfwGetCursorPos(windowHandle),
delta = MouseScrollUnit.Line(
if (yoffset != 0.0) -3 * yoffset.toFloat() else -3 * xoffset.toFloat()
),
orientation = if (yoffset != 0.0) MouseScrollOrientation.Vertical else MouseScrollOrientation.Horizontal
)
}
glfwSetKeyCallback(windowHandle) { _, key, _, action, _ ->
val awtId = when (action) {
GLFW_PRESS -> NativeKeyEvent.KEY_PRESSED
GLFW_REPEAT -> NativeKeyEvent.KEY_PRESSED
GLFW_RELEASE -> NativeKeyEvent.KEY_RELEASED
else -> error("Unknown type")
}
val awtKey = glfwToAwtKeyCode(key)
val time = System.nanoTime() / 1_000_000
// Note that we don't distinguish between Left/Right Shift, Del from numpad or not, etc.
// To distinguish we should change `location` parameter
sendKeyEvent(KeyEvent(awtId, time, getAwtMods(windowHandle), awtKey, 0.toChar(), NativeKeyEvent.KEY_LOCATION_STANDARD))
}
glfwSetCharCallback(windowHandle) { _, codepoint ->
for (char in Character.toChars(codepoint)) {
val time = System.nanoTime() / 1_000_000
sendKeyEvent(KeyEvent(NativeKeyEvent.KEY_TYPED, time, getAwtMods(windowHandle), 0, char, NativeKeyEvent.KEY_LOCATION_UNKNOWN))
}
}
glfwSetWindowContentScaleCallback(windowHandle) { _, xscale, _ ->
density = Density(xscale)
}
}
private fun glfwGetCursorPos(window: Long): Offset {
val x = DoubleArray(1)
val y = DoubleArray(1)
glfwGetCursorPos(window, x, y)
return Offset(x[0].toFloat(), y[0].toFloat())
}
// in the future versions of Compose we plan to get rid of the need of AWT events/components
val awtComponent = object : Component() {}
private fun KeyEvent(awtId: Int, time: Long, awtMods: Int, key: Int, char: Char, location: Int) = KeyEvent(
NativeKeyEvent(awtComponent, awtId, time, awtMods, key, char, location)
)
private fun MouseEvent(awtMods: Int) = MouseEvent(
awtComponent, 0, 0, awtMods, 0, 0, 1, false
)
private fun getAwtMods(windowHandle: Long): Int {
var awtMods = 0
if (glfwGetMouseButton(windowHandle, GLFW_MOUSE_BUTTON_1) == GLFW_PRESS)
awtMods = awtMods or InputEvent.BUTTON1_DOWN_MASK
if (glfwGetMouseButton(windowHandle, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS)
awtMods = awtMods or InputEvent.BUTTON2_DOWN_MASK
if (glfwGetMouseButton(windowHandle, GLFW_MOUSE_BUTTON_3) == GLFW_PRESS)
awtMods = awtMods or InputEvent.BUTTON3_DOWN_MASK
if (glfwGetMouseButton(windowHandle, GLFW_MOUSE_BUTTON_4) == GLFW_PRESS)
awtMods = awtMods or (1 shl 14)
if (glfwGetMouseButton(windowHandle, GLFW_MOUSE_BUTTON_5) == GLFW_PRESS)
awtMods = awtMods or (1 shl 15)
if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(windowHandle, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS)
awtMods = awtMods or InputEvent.CTRL_DOWN_MASK
if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(windowHandle, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS)
awtMods = awtMods or InputEvent.SHIFT_DOWN_MASK
if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || glfwGetKey(windowHandle, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS)
awtMods = awtMods or InputEvent.ALT_DOWN_MASK
return awtMods
}

113
experimental/lwjgl-integration/src/main/kotlin/GlfwKeyMapping.kt

@ -0,0 +1,113 @@
import org.lwjgl.glfw.GLFW.*
import java.awt.event.KeyEvent
fun glfwToAwtKeyCode(glfwKeyCode: Int): Int = when (glfwKeyCode) {
GLFW_KEY_SPACE -> KeyEvent.VK_SPACE
GLFW_KEY_APOSTROPHE -> KeyEvent.VK_QUOTE
GLFW_KEY_COMMA -> KeyEvent.VK_COMMA
GLFW_KEY_MINUS -> KeyEvent.VK_MINUS
GLFW_KEY_PERIOD -> KeyEvent.VK_PERIOD
GLFW_KEY_SLASH -> KeyEvent.VK_SLASH
GLFW_KEY_0 -> KeyEvent.VK_0
GLFW_KEY_1 -> KeyEvent.VK_1
GLFW_KEY_2 -> KeyEvent.VK_2
GLFW_KEY_3 -> KeyEvent.VK_3
GLFW_KEY_4 -> KeyEvent.VK_4
GLFW_KEY_5 -> KeyEvent.VK_5
GLFW_KEY_6 -> KeyEvent.VK_6
GLFW_KEY_7 -> KeyEvent.VK_7
GLFW_KEY_8 -> KeyEvent.VK_8
GLFW_KEY_9 -> KeyEvent.VK_9
GLFW_KEY_SEMICOLON -> KeyEvent.VK_SEMICOLON
GLFW_KEY_EQUAL -> KeyEvent.VK_EQUALS
GLFW_KEY_A -> KeyEvent.VK_A
GLFW_KEY_B -> KeyEvent.VK_B
GLFW_KEY_C -> KeyEvent.VK_C
GLFW_KEY_D -> KeyEvent.VK_D
GLFW_KEY_E -> KeyEvent.VK_E
GLFW_KEY_F -> KeyEvent.VK_F
GLFW_KEY_G -> KeyEvent.VK_G
GLFW_KEY_H -> KeyEvent.VK_H
GLFW_KEY_I -> KeyEvent.VK_I
GLFW_KEY_J -> KeyEvent.VK_J
GLFW_KEY_K -> KeyEvent.VK_K
GLFW_KEY_L -> KeyEvent.VK_L
GLFW_KEY_M -> KeyEvent.VK_M
GLFW_KEY_N -> KeyEvent.VK_N
GLFW_KEY_O -> KeyEvent.VK_O
GLFW_KEY_P -> KeyEvent.VK_P
GLFW_KEY_Q -> KeyEvent.VK_Q
GLFW_KEY_R -> KeyEvent.VK_R
GLFW_KEY_S -> KeyEvent.VK_S
GLFW_KEY_T -> KeyEvent.VK_T
GLFW_KEY_U -> KeyEvent.VK_U
GLFW_KEY_V -> KeyEvent.VK_V
GLFW_KEY_W -> KeyEvent.VK_W
GLFW_KEY_X -> KeyEvent.VK_X
GLFW_KEY_Y -> KeyEvent.VK_Y
GLFW_KEY_Z -> KeyEvent.VK_Z
GLFW_KEY_LEFT_BRACKET -> KeyEvent.VK_OPEN_BRACKET
GLFW_KEY_BACKSLASH -> KeyEvent.VK_BACK_SLASH
GLFW_KEY_RIGHT_BRACKET -> KeyEvent.VK_CLOSE_BRACKET
GLFW_KEY_GRAVE_ACCENT -> KeyEvent.VK_BACK_QUOTE
GLFW_KEY_ESCAPE -> KeyEvent.VK_ESCAPE
GLFW_KEY_ENTER -> KeyEvent.VK_ENTER
GLFW_KEY_TAB -> KeyEvent.VK_TAB
GLFW_KEY_BACKSPACE -> KeyEvent.VK_BACK_SPACE
GLFW_KEY_INSERT -> KeyEvent.VK_INSERT
GLFW_KEY_DELETE -> KeyEvent.VK_DELETE
GLFW_KEY_RIGHT -> KeyEvent.VK_RIGHT
GLFW_KEY_LEFT -> KeyEvent.VK_LEFT
GLFW_KEY_DOWN -> KeyEvent.VK_DOWN
GLFW_KEY_UP -> KeyEvent.VK_UP
GLFW_KEY_PAGE_UP -> KeyEvent.VK_PAGE_UP
GLFW_KEY_PAGE_DOWN -> KeyEvent.VK_PAGE_DOWN
GLFW_KEY_HOME -> KeyEvent.VK_HOME
GLFW_KEY_END -> KeyEvent.VK_END
GLFW_KEY_CAPS_LOCK -> KeyEvent.VK_CAPS_LOCK
GLFW_KEY_SCROLL_LOCK -> KeyEvent.VK_SCROLL_LOCK
GLFW_KEY_NUM_LOCK -> KeyEvent.VK_NUM_LOCK
GLFW_KEY_PRINT_SCREEN -> KeyEvent.VK_PRINTSCREEN
GLFW_KEY_PAUSE -> KeyEvent.VK_PAUSE
GLFW_KEY_F1 -> KeyEvent.VK_F1
GLFW_KEY_F2 -> KeyEvent.VK_F2
GLFW_KEY_F3 -> KeyEvent.VK_F3
GLFW_KEY_F4 -> KeyEvent.VK_F4
GLFW_KEY_F5 -> KeyEvent.VK_F5
GLFW_KEY_F6 -> KeyEvent.VK_F6
GLFW_KEY_F7 -> KeyEvent.VK_F7
GLFW_KEY_F8 -> KeyEvent.VK_F8
GLFW_KEY_F9 -> KeyEvent.VK_F9
GLFW_KEY_F10 -> KeyEvent.VK_F10
GLFW_KEY_F11 -> KeyEvent.VK_F11
GLFW_KEY_F12 -> KeyEvent.VK_F12
GLFW_KEY_F13 -> KeyEvent.VK_F13
GLFW_KEY_F14 -> KeyEvent.VK_F14
GLFW_KEY_F15 -> KeyEvent.VK_F15
GLFW_KEY_F16 -> KeyEvent.VK_F16
GLFW_KEY_F17 -> KeyEvent.VK_F17
GLFW_KEY_F18 -> KeyEvent.VK_F18
GLFW_KEY_F19 -> KeyEvent.VK_F19
GLFW_KEY_F20 -> KeyEvent.VK_F20
GLFW_KEY_F21 -> KeyEvent.VK_F21
GLFW_KEY_F22 -> KeyEvent.VK_F22
GLFW_KEY_F23 -> KeyEvent.VK_F23
GLFW_KEY_F24 -> KeyEvent.VK_F24
GLFW_KEY_KP_0 -> KeyEvent.VK_NUMPAD0
GLFW_KEY_KP_1 -> KeyEvent.VK_NUMPAD1
GLFW_KEY_KP_2 -> KeyEvent.VK_NUMPAD2
GLFW_KEY_KP_3 -> KeyEvent.VK_NUMPAD3
GLFW_KEY_KP_4 -> KeyEvent.VK_NUMPAD4
GLFW_KEY_KP_5 -> KeyEvent.VK_NUMPAD5
GLFW_KEY_KP_6 -> KeyEvent.VK_NUMPAD6
GLFW_KEY_KP_7 -> KeyEvent.VK_NUMPAD7
GLFW_KEY_KP_8 -> KeyEvent.VK_NUMPAD8
GLFW_KEY_KP_9 -> KeyEvent.VK_NUMPAD9
GLFW_KEY_LEFT_SHIFT -> KeyEvent.VK_SHIFT
GLFW_KEY_LEFT_CONTROL -> KeyEvent.VK_CONTROL
GLFW_KEY_LEFT_ALT -> KeyEvent.VK_ALT
GLFW_KEY_RIGHT_SHIFT -> KeyEvent.VK_SHIFT
GLFW_KEY_RIGHT_CONTROL -> KeyEvent.VK_CONTROL
GLFW_KEY_RIGHT_ALT -> KeyEvent.VK_ALT
else -> KeyEvent.VK_UNDEFINED
}

86
experimental/lwjgl-integration/src/main/kotlin/main.kt

@ -0,0 +1,86 @@
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import org.jetbrains.skia.*
import org.jetbrains.skia.FramebufferFormat.Companion.GR_GL_RGBA8
import org.jetbrains.skiko.FrameDispatcher
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL30.GL_FRAMEBUFFER_BINDING
import org.lwjgl.system.MemoryUtil.NULL
import kotlin.system.exitProcess
fun main() {
var width = 640
var height = 480
glfwInit()
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE)
val windowHandle: Long = glfwCreateWindow(width, height, "Compose LWJGL Demo", NULL, NULL)
glfwMakeContextCurrent(windowHandle)
glfwSwapInterval(1)
GL.createCapabilities()
val context = DirectContext.makeGL()
var surface = createSurface(width, height, context) // Skia Surface, bound to the OpenGL framebuffer
val glfwDispatcher = GlfwCoroutineDispatcher() // a custom coroutine dispatcher, in which Compose will run
glfwSetWindowCloseCallback(windowHandle) {
glfwDispatcher.stop()
}
lateinit var composeScene: ComposeScene
fun render() {
surface.canvas.clear(Color.WHITE)
composeScene.constraints = Constraints(maxWidth = width, maxHeight = height)
composeScene.render(surface.canvas, System.nanoTime())
context.flush()
glfwSwapBuffers(windowHandle)
}
val frameDispatcher = FrameDispatcher(glfwDispatcher) { render() }
val density = Density(glfwGetWindowContentScale(windowHandle))
composeScene = ComposeScene(glfwDispatcher, density, invalidate = frameDispatcher::scheduleFrame)
glfwSetWindowSizeCallback(windowHandle) { _, windowWidth, windowHeight ->
width = windowWidth
height = windowHeight
surface.close()
surface = createSurface(width, height, context)
glfwSwapInterval(0)
render()
glfwSwapInterval(1)
}
composeScene.subscribeToGLFWEvents(windowHandle)
composeScene.setContent { App() }
glfwShowWindow(windowHandle)
glfwDispatcher.runLoop()
composeScene.dispose()
glfwDestroyWindow(windowHandle)
exitProcess(0)
}
private fun createSurface(width: Int, height: Int, context: DirectContext): Surface {
val fbId = GL11.glGetInteger(GL_FRAMEBUFFER_BINDING)
val renderTarget = BackendRenderTarget.makeGL(width, height, 0, 8, fbId, GR_GL_RGBA8)
return Surface.makeFromBackendRenderTarget(
context, renderTarget, SurfaceOrigin.BOTTOM_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB
)
}
private fun glfwGetWindowContentScale(window: Long): Float {
val array = FloatArray(1)
glfwGetWindowContentScale(window, array, FloatArray(1))
return array[0]
}
Loading…
Cancel
Save