diff --git a/samples/SkiaMultiplatformSample/build.gradle.kts b/samples/SkiaMultiplatformSample/build.gradle.kts index 1c7088673..60760e480 100644 --- a/samples/SkiaMultiplatformSample/build.gradle.kts +++ b/samples/SkiaMultiplatformSample/build.gradle.kts @@ -8,16 +8,16 @@ buildscript { dependencies { // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.6.10")) + classpath(kotlin("gradle-plugin", version = "1.7.10")) } } plugins { - kotlin("multiplatform") version "1.6.10" + kotlin("multiplatform") version "1.7.10" id("org.jetbrains.gradle.apple.applePlugin") version "222.849-0.15.1" } -val coroutinesVersion = "1.5.2" +val coroutinesVersion = "1.6.4" repositories { mavenLocal() @@ -61,7 +61,7 @@ val unzipTask = tasks.register("unzipWasm", Copy::class) { } kotlin { - val targets = mutableListOf() + val macOSTargets = mutableListOf() if (hostOs == "macos") { val nativeHostTarget = when (host) { @@ -69,10 +69,10 @@ kotlin { "macos-arm64" -> macosArm64() else -> throw GradleException("Host OS is not supported yet") } - targets.add(nativeHostTarget) + macOSTargets.add(nativeHostTarget) - targets.add(iosX64()) - targets.add(iosArm64()) + macOSTargets.add(iosX64()) + macOSTargets.add(iosArm64()) ios { binaries { @@ -99,7 +99,7 @@ kotlin { binaries.executable() } - targets.forEach { + macOSTargets.forEach { it.apply { binaries { executable { @@ -114,6 +114,19 @@ kotlin { } } + if (hostOs == "windows") { + mingwX64 { + binaries { + executable { + entryPoint = "org.jetbrains.skiko.sample.main" + freeCompilerArgs += listOf( + "-linker-option", "-lopengl32" + ) + } + } + } + } + sourceSets { val commonMain by getting { dependencies { @@ -172,6 +185,16 @@ kotlin { val iosArm64Main by getting { dependsOn(iosMain) } + } else if (hostOs == "windows") { + val mingwMain by creating { + dependsOn(nativeMain) + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + } + } + val mingwX64Main by getting { + dependsOn(mingwMain) + } } } } diff --git a/samples/SkiaMultiplatformSample/gradle.properties b/samples/SkiaMultiplatformSample/gradle.properties index 8e07c78f4..dd315bc97 100644 --- a/samples/SkiaMultiplatformSample/gradle.properties +++ b/samples/SkiaMultiplatformSample/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official -kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.native.enableDependencyPropagation=false +kotlin.native.binary.memoryModel=experimental + org.gradle.jvmargs=-Xmx3G -XX:MaxMetaspaceSize=512m diff --git a/samples/SkiaMultiplatformSample/src/mingwX64Main/kotlin/org/jetbrains/skiko/sample/App.kt b/samples/SkiaMultiplatformSample/src/mingwX64Main/kotlin/org/jetbrains/skiko/sample/App.kt new file mode 100644 index 000000000..c9aacc7bd --- /dev/null +++ b/samples/SkiaMultiplatformSample/src/mingwX64Main/kotlin/org/jetbrains/skiko/sample/App.kt @@ -0,0 +1,71 @@ +package org.jetbrains.skiko.sample + +import kotlinx.cinterop.* +import org.jetbrains.skiko.GenericSkikoView +import org.jetbrains.skiko.SkiaLayer +import platform.windows.* + +fun makeApp(skiaLayer: SkiaLayer) = Clocks(skiaLayer) + +lateinit var skiaLayer: SkiaLayer + +fun wndProc(hwnd: HWND?, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT { + if(msg == WM_DESTROY.toUInt()) { + PostQuitMessage(0) + return 0 + } + return skiaLayer.windowProc(hwnd, msg, wParam, lParam) +} + +fun main() { + + skiaLayer = SkiaLayer() + + memScoped { + val lpszClassName = "SkiaMultiplatformSample" + + val wc = alloc() + wc.cbSize = sizeOf().toUInt() + wc.lpfnWndProc = staticCFunction(::wndProc) + wc.style = (CS_HREDRAW or CS_VREDRAW or CS_OWNDC).toUInt() + wc.cbClsExtra = 0 + wc.cbWndExtra = 0 + wc.hInstance = null + wc.hIcon = null + wc.hCursor = (LoadCursor!!)(null, IDC_ARROW) + wc.lpszMenuName = null + wc.lpszClassName = lpszClassName.wcstr.ptr + wc.hIconSm = null + + if (RegisterClassEx!!(wc.ptr) == 0u.toUShort()) { + println("could not register") + return + } + + val hwnd = CreateWindowExA( + 0, lpszClassName, "SkikoNative", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, + 640, 480, + null, null, null, null + )!! + skiaLayer.attachTo(hwnd) + ShowWindow(hwnd, SW_SHOW) + } + + skiaLayer.skikoView = GenericSkikoView(skiaLayer, makeApp(skiaLayer)) + + memScoped { + val msg = alloc() + msg.message = 0u + while (GetMessage!!(msg.ptr, null, 0u, 0u) > 0) { + if(msg.message == WM_QUIT.toUInt()) { + break + } + TranslateMessage(msg.ptr) + DispatchMessageA(msg.ptr) + } + } + + skiaLayer.detach() +} \ No newline at end of file diff --git a/skiko/build.gradle.kts b/skiko/build.gradle.kts index 064ec1120..403feef00 100644 --- a/skiko/build.gradle.kts +++ b/skiko/build.gradle.kts @@ -1,21 +1,21 @@ import de.undercouch.gradle.tasks.download.Download import org.gradle.crypto.checksum.Checksum -import org.gradle.api.tasks.testing.AbstractTestTask import org.jetbrains.compose.internal.publishing.MavenCentralProperties import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool import org.gradle.api.tasks.testing.logging.TestExceptionFormat plugins { - kotlin("multiplatform") version "1.6.10" - id("org.jetbrains.dokka") version "1.6.10" + kotlin("multiplatform") version "1.7.10" + id("org.jetbrains.dokka") version "1.7.10" `maven-publish` signing id("org.gradle.crypto.checksum") version "1.1.0" id("de.undercouch.download") version "4.1.2" } -val coroutinesVersion = "1.5.2" +val coroutinesVersion = "1.6.4" fun targetSuffix(os: OS, arch: Arch): String { return "${os.id}_${arch.id}" @@ -189,6 +189,17 @@ fun compileNativeBridgesTask(os: OS, arch: Arch, isArm64Simulator: Boolean): Tas *skiaPreprocessorFlags(OS.Linux) )) } + OS.MinGW -> { + flags.set(listOf( + *buildType.clangFlags, + "-fno-rtti", + "-fno-exceptions", + "-fvisibility=hidden", + "-fvisibility-inlines-hidden", + "-D_GLIBCXX_USE_CXX11_ABI=0", + *skiaPreprocessorFlags(OS.Linux) + )) + } else -> throw GradleException("$os not yet supported") } @@ -258,6 +269,7 @@ kotlin { configureNativeTarget(OS.MacOS, Arch.X64, macosX64()) configureNativeTarget(OS.MacOS, Arch.Arm64, macosArm64()) configureNativeTarget(OS.Linux, Arch.X64, linuxX64()) + configureNativeTarget(OS.MinGW, Arch.X64, mingwX64()) configureNativeTarget(OS.IOS, Arch.Arm64, iosArm64()) configureNativeTarget(OS.IOS, Arch.X64, iosX64()) configureNativeTarget(OS.IOS, Arch.Arm64, iosSimulatorArm64()) @@ -368,6 +380,21 @@ kotlin { val linuxX64Test by getting { dependsOn(linuxTest) } + val mingwMain by creating { + dependsOn(nativeJsMain) + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + } + } + val mingwTest by creating { + dependsOn(nativeTest) + } + val mingwX64Main by getting { + dependsOn(mingwMain) + } + val mingwX64Test by getting { + dependsOn(mingwTest) + } val darwinMain by creating { dependsOn(nativeMain) } @@ -434,7 +461,7 @@ fun configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) { val skiaDir = unpackedSkia.absolutePath val bridgesLibrary = "$buildDir/nativeBridges/static/$targetString/skiko-native-bridges-$targetString.a" - val allLibraries = skiaStaticLibraries(skiaDir, targetString) + bridgesLibrary + val allLibraries = skiaStaticLibraries(os, skiaDir, targetString) + bridgesLibrary target.compilations.all { val skiaBinDir = "$skiaDir/out/${buildType.id}-$targetString" @@ -459,6 +486,9 @@ fun configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) { "-linker-option", "$skiaBinDir/libskunicode.a", "-linker-option", "$skiaBinDir/libskia.a" ) + OS.MinGW -> mutableListOf( + "-linker-option", "-lopengl32" + ) else -> mutableListOf() } if (skiko.includeTestHelpers) { @@ -489,7 +519,7 @@ fun configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) { val staticLib = "$outDir/skiko-native-bridges-$targetString.a" workingDir = File(outDir) when (os) { - OS.Linux -> { + OS.Linux, OS.MinGW -> { executable = "ar" argumentProviders.add { listOf("-crs", staticLib) } } @@ -504,7 +534,9 @@ fun configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) { outputs.dir(outDir) } target.compilations.all { - compileKotlinTask.dependsOn(linkTask) + compileKotlinTaskProvider.configure { + dependsOn(linkTask) + } } } @@ -586,30 +618,53 @@ fun skiaPreprocessorFlags(os: OS): Array { return (base + perOs).toTypedArray() } -fun skiaStaticLibraries(skiaDir: String, targetString: String): List { +fun skiaStaticLibraries(os: OS, skiaDir: String, targetString: String): List { val skiaBinSubdir = "$skiaDir/out/${buildType.id}-$targetString" - return listOf( - "libskresources.a", - "libparticles.a", - "libskparagraph.a", - "libskia.a", - "libicu.a", - "libskottie.a", - "libsvg.a", - "libpng.a", - "libfreetype2.a", - "libwebp_sse41.a", - "libsksg.a", - "libskunicode.a", - "libwebp.a", - "libdng_sdk.a", - "libpiex.a", - "libharfbuzz.a", - "libexpat.a", - "libzlib.a", - "libjpeg.a", - "libskshaper.a" - ).map{ + return when(os) { + OS.MinGW -> listOf( + "libexpat.a", + "libfreetype2.a", + "libharfbuzz.a", + "libicu.a", + "libjpeg.a", + "libparticles.a", + "libpng.a", + "libskcms.a", + "libskia.a", + "libskottie.a", + "libskparagraph.a", + "libskresources.a", + "libsksg.a", + "libskshaper.a", + "libskunicode.a", + "libsvg.a", + "libwebp.a", + "libwebp_sse41.a", + "libzlib.a", + ) + else -> listOf( + "libskresources.a", + "libparticles.a", + "libskparagraph.a", + "libskia.a", + "libicu.a", + "libskottie.a", + "libsvg.a", + "libpng.a", + "libfreetype2.a", + "libwebp_sse41.a", + "libsksg.a", + "libskunicode.a", + "libwebp.a", + "libdng_sdk.a", + "libpiex.a", + "libharfbuzz.a", + "libexpat.a", + "libzlib.a", + "libjpeg.a", + "libskshaper.a" + ) + }.map { "$skiaBinSubdir/$it" } } @@ -864,7 +919,7 @@ fun createCompileJvmBindingsTask( "-fPIC" ) } - OS.Wasm, OS.IOS -> error("Should not reach here") + OS.Wasm, OS.IOS, OS.MinGW -> error("Should not reach here") } flags.set( @@ -967,6 +1022,16 @@ fun createLinkJvmBindings( if (buildType == SkiaBuildType.DEBUG) add("dxgi.lib") }.toTypedArray() } + OS.MinGW -> { + osFlags = arrayOf( + "-shared", + "-static-libstdc++", + "-static-libgcc", + "-lGL", + "-lfontconfig", + "-lopengl32" + ) + } OS.Android -> { osFlags = arrayOf( "-shared", @@ -1023,7 +1088,7 @@ fun KotlinTarget.generateVersion( val compilation = compilations["main"] ?: error("Could not find 'main' compilation for target '$this'") compilation.compileKotlinTaskProvider.configure { dependsOn(generateVersionTask) - (this as AbstractCompile).source(generatedDir.get().asFile) + (this as KotlinCompileTool).source(generatedDir.get().asFile) } } diff --git a/skiko/buildSrc/src/main/kotlin/CompileSkikoCppTask.kt b/skiko/buildSrc/src/main/kotlin/CompileSkikoCppTask.kt index 210e611c6..4a50c43e4 100644 --- a/skiko/buildSrc/src/main/kotlin/CompileSkikoCppTask.kt +++ b/skiko/buildSrc/src/main/kotlin/CompileSkikoCppTask.kt @@ -261,7 +261,7 @@ abstract class CompileSkikoCppTask() : AbstractSkikoNativeToolTask() { override fun configureArgs() = super.configureArgs().apply { arg("-c") - repeatedArg("-I", headersDirs) + repeatedArg("-I", headersDirs.map { if(buildTargetOS.get() == OS.MinGW) it.absolutePath.replace("\\", "/") else it }) // todo: ensure that flags do not start with '-I' (all headers should be added via [headersDirs]) rawArgs(flags.get()) } diff --git a/skiko/buildSrc/src/main/kotlin/properties.kt b/skiko/buildSrc/src/main/kotlin/properties.kt index 15074b9f3..15aae44f9 100644 --- a/skiko/buildSrc/src/main/kotlin/properties.kt +++ b/skiko/buildSrc/src/main/kotlin/properties.kt @@ -8,6 +8,7 @@ enum class OS( Linux("linux", arrayOf()), Android("android", arrayOf()), Windows("windows", arrayOf()), + MinGW("mingw", arrayOf()), MacOS("macos", arrayOf("-mmacosx-version-min=10.13")), Wasm("wasm", arrayOf()), IOS("ios", arrayOf()) @@ -25,6 +26,7 @@ val OS.isCompatibleWithHost: Boolean get() = when (this) { OS.Linux -> hostOs == OS.Linux OS.Windows -> hostOs == OS.Windows + OS.MinGW -> hostOs == OS.Windows OS.MacOS, OS.IOS -> hostOs == OS.MacOS OS.Wasm -> true OS.Android -> true @@ -39,6 +41,7 @@ fun compilerForTarget(os: OS, arch: Arch): String = } OS.Android -> "clang++" OS.Windows -> "cl.exe" + OS.MinGW -> "g++.exe" OS.MacOS, OS.IOS -> "clang++" OS.Wasm -> "emcc" } @@ -50,6 +53,7 @@ val OS.dynamicLibExt: String get() = when (this) { OS.Linux, OS.Android -> ".so" OS.Windows -> ".dll" + OS.MinGW -> ".dll" OS.MacOS, OS.IOS -> ".dylib" OS.Wasm -> ".wasm" } diff --git a/skiko/ci/build.gradle.kts b/skiko/ci/build.gradle.kts index f7530fa6b..d20ff2723 100644 --- a/skiko/ci/build.gradle.kts +++ b/skiko/ci/build.gradle.kts @@ -20,6 +20,7 @@ val skikoArtifactIds: List = SkikoArtifacts.nativeArtifactIdFor(OS.MacOS, Arch.X64), SkikoArtifacts.nativeArtifactIdFor(OS.IOS, Arch.X64), SkikoArtifacts.nativeArtifactIdFor(OS.IOS, Arch.Arm64), + SkikoArtifacts.nativeArtifactIdFor(OS.Windows, Arch.X64), SkikoArtifacts.nativeArtifactIdFor(OS.IOS, Arch.Arm64, isIosSim = true), ) diff --git a/skiko/gradle.properties b/skiko/gradle.properties index 659dc8e37..3c2b68832 100644 --- a/skiko/gradle.properties +++ b/skiko/gradle.properties @@ -1,9 +1,9 @@ kotlin.code.style=official deploy.version=0.0.0 -kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.native.enableDependencyPropagation=false +kotlin.native.binary.memoryModel=experimental dependencies.skia.windows-x64=m105-f204b137b9-5 +dependencies.skia.mingw-x64=m105-f204b137b9-5 dependencies.skia.linux-x64=m105-f204b137b9-5 dependencies.skia.macos-x64=m105-f204b137b9-5 dependencies.skia.linux-arm64=m105-f204b137b9-5 diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Actuals.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Actuals.native.kt new file mode 100644 index 000000000..138211318 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Actuals.native.kt @@ -0,0 +1,54 @@ +package org.jetbrains.skia + +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.CPointer +import org.jetbrains.skia.impl.InteropPointer +import org.jetbrains.skia.impl.withResult + +actual abstract class OutputStream + +internal actual fun commonSynchronized(lock: Any, block: () -> R) { + block() +} + +internal actual fun String.intCodePoints(): IntArray = IntArray(this.length) { this[it].code } + +actual class Pattern constructor(regex: String) { + private val _regex = Regex(regex) + + actual fun split(input: CharSequence): Array = _regex.split(input).toTypedArray() + actual fun matcher(input: CharSequence): Matcher = Matcher(_regex, input) +} + +actual class Matcher constructor(private val regex: Regex, private val input: CharSequence) { + + private val matches: Boolean by lazy { + regex.matches(input) + } + + private val groups: MatchGroupCollection? by lazy { regex.matchEntire(input)?.groups } + + actual fun group(ix: Int): String? = groups?.get(ix)?.value + actual fun matches(): Boolean = matches +} + +private val LANG by lazy { + val localeFromICU = uloc_getDefault() + var length = 0 + val maxLength = 128 + val langTag = withResult(ByteArray(maxLength)) { + length = uloc_toLanguageTag(localeFromICU, it, maxLength, false, toInterop(intArrayOf(0))) + }.decodeToString(0, length) + langTag.ifEmpty { "en-US" } +} + +internal actual fun defaultLanguageTag(): String = LANG + +internal actual fun compilePattern(regex: String): Pattern = Pattern(regex) + +actual typealias ExternalSymbolName = kotlin.native.SymbolName + +@SymbolName("uloc_getDefault") +private external fun uloc_getDefault(): CPointer +@SymbolName("uloc_toLanguageTag") +private external fun uloc_toLanguageTag(localeId: CPointer, buffer: InteropPointer, size: Int, strict: Boolean, err: InteropPointer): Int \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Data.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Data.native.kt new file mode 100644 index 000000000..e6c2563a4 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Data.native.kt @@ -0,0 +1,15 @@ +package org.jetbrains.skia + +import org.jetbrains.skia.impl.Stats +import org.jetbrains.skia.impl.interopScope + +/** + * Create a new dataref the file with the specified path. + * If the file cannot be opened, this returns null. + */ +fun Data.Companion.makeFromFileName(path: String?): Data { + Stats.onNativeCall() + interopScope { + return Data(_nMakeFromFileName(toInterop(path))) + } +} \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Typeface.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Typeface.native.kt new file mode 100644 index 000000000..2a0ca4ce7 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/Typeface.native.kt @@ -0,0 +1,18 @@ +package org.jetbrains.skia + +import org.jetbrains.skia.impl.Native +import org.jetbrains.skia.impl.Stats +import org.jetbrains.skia.impl.interopScope + +/** + * @return a new typeface given a file + * @throws IllegalArgumentException If the file does not exist, or is not a valid font file + */ +fun Typeface.Companion.makeFromFile(path: String, index: Int = 0): Typeface { + Stats.onNativeCall() + interopScope { + val ptr = _nMakeFromFile(toInterop(path), index) + require(ptr != Native.NullPointer) { "Failed to create Typeface from path=\"$path\" index=$index" } + return Typeface(ptr) + } +} \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Library.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Library.native.kt new file mode 100644 index 000000000..ea4995087 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Library.native.kt @@ -0,0 +1,10 @@ +package org.jetbrains.skia.impl + +actual class Library { + actual companion object { + actual fun staticLoad() { + // Not much here for now. + // We link statically for native. + } + } +} diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Managed.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Managed.native.kt new file mode 100644 index 000000000..496d482e8 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Managed.native.kt @@ -0,0 +1,62 @@ +package org.jetbrains.skia.impl + +import kotlinx.cinterop.nativeNullPtr +import org.jetbrains.skia.ExternalSymbolName +import kotlin.native.concurrent.AtomicNativePtr +import kotlin.native.concurrent.freeze +import kotlin.native.internal.createCleaner + +private class FinalizationThunk(private val finalizer: NativePointer, val className: String, obj: NativePointer) { + private var obj = AtomicNativePtr(obj) + + fun clean() { + val ptr = obj.value + if (ptr != nativeNullPtr && obj.compareAndSet(ptr, nativeNullPtr)) { + Stats.onDeallocated(className) + Stats.onNativeCall() + _nInvokeFinalizer(finalizer, ptr) + } + } + val isActive get() = + obj.value != nativeNullPtr +} + +actual abstract class Managed actual constructor( + ptr: NativePointer, finalizer: NativePointer, managed: Boolean) : Native(ptr) { + + private val thunk: FinalizationThunk? = if (managed) { + require(ptr != NullPointer) { "Managed ptr is nullptr" } + require(finalizer != NullPointer) { "Managed finalizer is nullptr" } + val className = this::class.simpleName ?: "" + Stats.onAllocated(className) + FinalizationThunk(finalizer, className, ptr).freeze() + } else null + + @OptIn(ExperimentalStdlibApi::class) + private val cleaner = if (managed) { + createCleaner(thunk) { + it?.clean() + } + } else null + + actual open fun close() { + require(_ptr != NullPointer) { + "Object already closed: ${this::class.simpleName}, _ptr=$_ptr" + } + requireNotNull(thunk) { + "Object is not managed in K/N runtime, can't close(): ${this::class.simpleName}, _ptr=$_ptr" + } + require(thunk.isActive) { + "Object is closed already, can't close(): ${this::class.simpleName}, _ptr=$_ptr" + } + + thunk.clean() + _ptr = NullPointer + } + + actual open val isClosed: Boolean + get() = _ptr == NullPointer +} + +@ExternalSymbolName("org_jetbrains_skia_impl_Managed__invokeFinalizer") +internal external fun _nInvokeFinalizer(finalizer: NativePointer, ptr: NativePointer) diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Native.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Native.native.kt new file mode 100644 index 000000000..859061532 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Native.native.kt @@ -0,0 +1,286 @@ +package org.jetbrains.skia.impl + +import kotlinx.cinterop.* +import org.jetbrains.skia.ExternalSymbolName +import kotlin.native.internal.NativePtr + +actual abstract class Native actual constructor(ptr: NativePointer) { + internal actual var _ptr: NativePointer + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (null == other) return false + if (other !is Native) return false + return if (_ptr == other._ptr) true else nativeEquals(other) + } + + override fun hashCode(): Int { + return _ptr.toLong().hashCode() + } + + internal actual open fun nativeEquals(other: Native?): Boolean { + return false + } + + actual companion object { + init { + initCallbacks( + staticCFunction(::callBooleanCallback), + staticCFunction(::callIntCallback), + staticCFunction(::callNativePtrCallback), + staticCFunction(::callVoidCallback), + staticCFunction(::disposeCallback), + ) + } + + actual val NullPointer: NativePointer + get() = NativePtr.NULL + } + + actual override fun toString(): String { + return this::class.simpleName + "(_ptr=0x" + _ptr.toString() + ")" + } + + init { + if (ptr == NativePtr.NULL) throw RuntimeException("Can't wrap nullptr") + _ptr = ptr + } +} + +actual typealias NativePointer = NativePtr +actual typealias InteropPointer = NativePtr + +internal actual fun reachabilityBarrier(obj: Any?) { + // TODO: implement native barrier +} + +internal actual inline fun interopScope(block: InteropScope.() -> T): T { + val scope = InteropScope() + try { + return scope.block() + } finally { + scope.release() + } +} + +internal actual class InteropScope actual constructor() { + actual fun toInterop(string: String?): InteropPointer { + return if (string != null) { + // encodeToByteArray encodes to utf8 + val utf8 = string.encodeToByteArray() + // TODO Remove array copy, use `skString(data, length)` instead of `skString(data)` + val pinned = utf8.copyOf(utf8.size + 1).pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun toInterop(array: ByteArray?): InteropPointer { + return if (array != null && array.isNotEmpty()) { + val pinned = array.pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun toInteropForResult(array: ByteArray?): InteropPointer = toInterop(array) + + actual fun InteropPointer.fromInterop(result: ByteArray) {} + + actual fun toInterop(array: ShortArray?): InteropPointer { + return if (array != null && array.isNotEmpty()) { + val pinned = array.pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun toInteropForResult(array: ShortArray?): InteropPointer = toInterop(array) + + actual fun InteropPointer.fromInterop(result: ShortArray) {} + + actual fun toInterop(array: IntArray?): InteropPointer { + return if (array != null && array.isNotEmpty()) { + val pinned = array.pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun toInteropForResult(array: IntArray?): InteropPointer = toInterop(array) + + actual fun InteropPointer.fromInterop(result: IntArray) {} + + actual fun toInterop(array: LongArray?): InteropPointer { + return if (array != null && array.isNotEmpty()) { + val pinned = array.pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun InteropPointer.fromInterop(result: LongArray) {} + + actual fun toInterop(array: FloatArray?): InteropPointer { + return if (array != null && array.isNotEmpty()) { + val pinned = array.pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun toInteropForResult(array: FloatArray?): InteropPointer = toInterop(array) + + actual fun InteropPointer.fromInterop(result: FloatArray) {} + + actual fun toInterop(array: DoubleArray?): InteropPointer { + return if (array != null && array.isNotEmpty()) { + val pinned = array.pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun toInteropForResult(array: DoubleArray?): InteropPointer = toInterop(array) + + actual fun InteropPointer.fromInterop(result: DoubleArray) {} + + actual fun toInterop(array: NativePointerArray?): InteropPointer { + return if (array != null && array.size > 0) { + // We pass it as LongArray via boundary. + val pinned = array.backing.pin() + elements.add(pinned) + val result = pinned.addressOf(0).rawValue + result + } else { + NativePtr.NULL + } + } + + actual fun toInteropForResult(array: NativePointerArray?): InteropPointer = toInterop(array) + + actual fun InteropPointer.fromInterop(result: NativePointerArray) {} + + actual fun toInterop(stringArray: Array?): InteropPointer { + if (stringArray == null || stringArray.isEmpty()) return NativePtr.NULL + + val pins = stringArray.toList() + .map { it.encodeToByteArray().pin() } + + val nativePointerArray = NativePointerArray(stringArray.size) + pins.forEachIndexed { index, pin -> + elements.add(pin) + nativePointerArray[index] = pin.addressOf(0).rawValue + } + return toInterop(nativePointerArray) + } + + actual inline fun InteropPointer.fromInterop(decoder: ArrayInteropDecoder): Array { + val size = decoder.getArraySize(this) + val result = Array(size) { + decoder.getArrayElement(this, it) + } + decoder.disposeArray(this) + return result + } + + actual fun InteropPointer.fromInteropNativePointerArray(): NativePointerArray { + TODO("implement native fromInteropNativePointerArray") + } + + actual fun toInteropForArraysOfPointers(interopPointers: Array): InteropPointer { + return toInterop(interopPointers.map { it.toLong() }.toLongArray()) + } + + actual fun callback(callback: (() -> Unit)?) = callbackImpl(callback) + actual fun intCallback(callback: (() -> Int)?) = callbackImpl(callback) + actual fun nativePointerCallback(callback: (() -> NativePointer)?) = callbackImpl(callback) + actual fun interopPointerCallback(callback: (() -> InteropPointer)?) = callbackImpl(callback) + actual fun booleanCallback(callback: (() -> Boolean)?) = callbackImpl(callback) + + actual fun virtual(method: () -> Unit) = callbackImpl(method) + actual fun virtualInt(method: () -> Int) = callbackImpl(method) + actual fun virtualNativePointer(method: () -> NativePointer) = callbackImpl(method) + actual fun virtualInteropPointer(method: () -> InteropPointer) = callbackImpl(method) + actual fun virtualBoolean(method: () -> Boolean) = callbackImpl(method) + + actual fun release() { + elements.forEach { + it.unpin() + } + } + + private fun callbackImpl(callback: (() -> T)?): InteropPointer = callback?.let { + val ptr = StableRef.create(it).asCPointer() + NativePtr.NULL.plus(ptr.toLong()) + } ?: NativePtr.NULL + + private val elements = mutableListOf>() +} + +// Ugly! NativePtrArray in stdlib is unfortunately internal, don't have ctor and cannot be used. +actual class NativePointerArray actual constructor(size: Int) { + internal val backing = LongArray(size) + actual operator fun get(index: Int): NativePointer { + return NativePtr.NULL + backing[index] + } + + actual operator fun set(index: Int, value: NativePointer) { + backing[index] = value.toLong() + } + + actual val size: Int + get() = backing.size +} + +// Callbacks support + +private fun callVoidCallback(ptr: COpaquePointer) { + ptr.asStableRef<() -> Unit>().get().invoke() +} + +private fun callBooleanCallback(ptr: COpaquePointer): Boolean { + return ptr.asStableRef<() -> Boolean>().get().invoke() +} + +private fun callIntCallback(ptr: COpaquePointer): Int { + return ptr.asStableRef<() -> Int>().get().invoke() +} + +private fun callNativePtrCallback(ptr: COpaquePointer): Long { + return ptr.asStableRef<() -> NativePointer>().get().invoke().toLong() +} + +private fun disposeCallback(ptr: COpaquePointer) { + ptr.asStableRef().dispose() +} + +@ExternalSymbolName("skiko_initCallbacks") +private external fun initCallbacks( + callBoolean: COpaquePointer, + callInt: COpaquePointer, + callNativePointer: COpaquePointer, + callVoid: COpaquePointer, + dispose: COpaquePointer +) diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/RefCnt.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/RefCnt.native.kt new file mode 100644 index 000000000..4d2135882 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/RefCnt.native.kt @@ -0,0 +1,30 @@ +package org.jetbrains.skia.impl + +import org.jetbrains.skia.ExternalSymbolName + +actual abstract class RefCnt : Managed { + protected actual constructor(ptr: NativePointer) : super(ptr, _FinalizerHolder.PTR, true) {} + protected actual constructor(ptr: NativePointer, allowClose: Boolean) : super(ptr, _FinalizerHolder.PTR, allowClose) + + actual val refCount: Int + get() = try { + Stats.onNativeCall() + _nGetRefCount(_ptr) + } finally { + reachabilityBarrier(this) + } + + override fun toString(): String { + val s = super.toString() + return s.substring(0, s.length - 1) + ", refCount=" + refCount + ")" + } + + private object _FinalizerHolder { + val PTR = RefCnt_nGetFinalizer() + } +} + +@ExternalSymbolName("org_jetbrains_skia_impl_RefCnt__getFinalizer") +internal actual external fun RefCnt_nGetFinalizer(): NativePointer +@ExternalSymbolName("org_jetbrains_skia_impl_RefCnt__getRefCount") +private external fun _nGetRefCount(ptr: NativePointer): Int diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Stats.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Stats.native.kt new file mode 100644 index 000000000..af2d74268 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/impl/Stats.native.kt @@ -0,0 +1,27 @@ +package org.jetbrains.skia.impl + +import kotlin.native.concurrent.AtomicLong + +actual object Stats { + val enabled = false + val nativeCalls = AtomicLong(0) + val allocated = AtomicLong(0) + + actual fun onNativeCall() { + if (enabled) nativeCalls.increment() + } + + actual fun onAllocated(className: String) { + if (enabled) { + allocated.increment() + println("AFTER ALLOC: $allocated") + } + } + + actual fun onDeallocated(className: String) { + if (enabled) { + allocated.decrement() + println("AFTER DEALLOC: $allocated") + } + } +} \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/skottie/Animation.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/skottie/Animation.native.kt new file mode 100644 index 000000000..9faa4c77b --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/skottie/Animation.native.kt @@ -0,0 +1,15 @@ +package org.jetbrains.skia.skottie + +import org.jetbrains.skia.impl.Native +import org.jetbrains.skia.impl.Stats +import org.jetbrains.skia.impl.interopScope + + +fun Animation.Companion.makeFromFile(path: String): Animation { + Stats.onNativeCall() + interopScope { + val ptr = _nMakeFromFile(toInterop(path)) + require(ptr != Native.NullPointer) { "Failed to create Animation from path=\"$path\"" } + return Animation(ptr) + } +} \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skia/skottie/AnimationBuilder.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/skottie/AnimationBuilder.native.kt new file mode 100644 index 000000000..0abd39d41 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skia/skottie/AnimationBuilder.native.kt @@ -0,0 +1,17 @@ +package org.jetbrains.skia.skottie + +import org.jetbrains.skia.impl.Native +import org.jetbrains.skia.impl.Stats +import org.jetbrains.skia.impl.interopScope +import org.jetbrains.skia.impl.reachabilityBarrier + +fun AnimationBuilder.buildFromFile(path: String): Animation { + return try { + Stats.onNativeCall() + val ptr = interopScope { _nBuildFromFile(_ptr, toInterop(path)) } + require(ptr != Native.NullPointer) { "Failed to create Animation from path: $path" } + Animation(ptr) + } finally { + reachabilityBarrier(this) + } +} \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Actuals.mingw.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Actuals.mingw.kt new file mode 100644 index 000000000..39dba5f26 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Actuals.mingw.kt @@ -0,0 +1,56 @@ +package org.jetbrains.skiko + +import platform.windows.* + +internal actual inline fun maybeSynchronized(lock: Any, block: () -> R): R = + block() + +actual fun currentNanoTime(): Long = kotlin.system.getTimeNanos() + +internal actual fun URIHandler_openUri(uri: String) { + ShellExecuteA(null, "open", uri, null, null, SW_SHOWNORMAL) +} + +internal actual fun ClipboardManager_setText(text: String) { + TODO("Implement ClipboardManager_setText() on Linux") +} + +internal actual fun ClipboardManager_getText(): String? { + TODO("Implement ClipboardManager_getText() on Linux") +} + +private val cursorCache = mutableMapOf() + +actual data class Cursor(val value: LPWSTR) + +internal actual fun CursorManager_setCursor(component: Any, cursor: Cursor) { + val c = cursorCache[cursor.value] + if (c == null) { + val hCursor = LoadCursorW(null, cursor.value) ?: throw Error("Failed to load cursor") + cursorCache[cursor.value] = hCursor + SetCursor(hCursor) + } else { + SetCursor(c) + } +} + +internal actual fun CursorManager_getCursor(component: Any): Cursor? { + val cursor = GetCursor() ?: return null + for (entry in cursorCache) { + if(entry.value == cursor) { + return Cursor(entry.key) + } + } + return null +} + +internal actual fun getCursorById(id: PredefinedCursorsId): Cursor = + when (id) { + PredefinedCursorsId.DEFAULT -> Cursor(IDC_ARROW!!) + PredefinedCursorsId.CROSSHAIR -> Cursor(IDC_CROSS!!) + PredefinedCursorsId.HAND -> Cursor(IDC_HAND!!) + PredefinedCursorsId.TEXT -> Cursor(IDC_IBEAM!!) + } + +actual val currentSystemTheme: SystemTheme + get() = SystemTheme.UNKNOWN // TODO Check registry (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme) diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Event.mingw.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Event.mingw.kt new file mode 100644 index 000000000..d9b33293e --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Event.mingw.kt @@ -0,0 +1,134 @@ +package org.jetbrains.skiko + +import platform.windows.* + +private fun LOWORD(lParam: LPARAM): UInt = lParam.toUInt() and 0xffffu +private fun HIWORD(lParam: LPARAM): UInt = lParam.toUInt() shr 16 + +private fun LOBYTE(lParam: LPARAM): UInt = lParam.toUInt() and 0xffu +private fun MAKEWORD(low: UInt, high: UInt): UInt = low or (high shl 16) + +operator fun SkikoMouseButtons.plus(other: SkikoMouseButtons): SkikoMouseButtons = + SkikoMouseButtons(this.value or other.value) + +operator fun SkikoInputModifiers.plus(other: SkikoInputModifiers): SkikoInputModifiers = + SkikoInputModifiers(this.value or other.value) + +operator fun SkikoInputModifiers.minus(other: SkikoInputModifiers): SkikoInputModifiers = + SkikoInputModifiers(this.value and other.value.inv()) + +actual data class SkikoPlatformKeyboardEvent( + val kind: SkikoKeyboardEventKind, + val virtualKey: WPARAM, + val modifiers: LPARAM, + val inputModifiers: SkikoInputModifiers +) { + val skikoEvent: SkikoKeyboardEvent + get() { + var vk = virtualKey.toInt() + if (vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU) { + val keyFlags = HIWORD(modifiers) + val isExtendedKey = (keyFlags and KF_EXTENDED.toUInt()) == KF_EXTENDED.toUInt() + if (isExtendedKey) { + when (vk) { + VK_SHIFT -> vk = VK_RSHIFT + VK_CONTROL -> vk = VK_RCONTROL + VK_MENU -> vk = VK_RMENU + } + } else { + when (vk) { + VK_SHIFT -> vk = VK_LSHIFT + VK_CONTROL -> vk = VK_LCONTROL + VK_MENU -> vk = VK_LMENU + } + } + } + return SkikoKeyboardEvent( + SkikoKey.valueOf(vk), + kind = kind, + platform = this + ) + } +} + +actual data class SkikoPlatformInputEvent( + val charCode: WPARAM, + val modifiers: LPARAM, + val inputModifiers: SkikoInputModifiers +) { + val skikoEvent: SkikoInputEvent + get() { + var vk = LOWORD(charCode.toLong()) + if (vk == VK_SHIFT.toUInt() || vk == VK_CONTROL.toUInt() || vk == VK_MENU.toUInt()) { + val keyFlags = HIWORD(modifiers) + var scanCode = LOBYTE(keyFlags.toLong()) + val isExtendedKey = (keyFlags and KF_EXTENDED.toUInt()) == KF_EXTENDED.toUInt() + if (isExtendedKey) { + scanCode = MAKEWORD(scanCode, 0xE0u) + } + val code = MapVirtualKeyA(scanCode, MAPVK_VK_TO_VSC_EX) + vk = LOWORD(code.toLong()) + } + return SkikoInputEvent( + charCode.toInt().toChar().toString(), + key = SkikoKey.valueOf(vk.toInt()), + kind = SkikoKeyboardEventKind.TYPE, + platform = this, + ) + } +} + +actual data class SkikoPlatformPointerEvent( + val kind: SkikoPointerEventKind, + val pressedButtons: WPARAM, + val position: LPARAM, + val button: SkikoMouseButtons = SkikoMouseButtons.NONE, + val inputModifiers: SkikoInputModifiers = SkikoInputModifiers.EMPTY, +) { + val skikoEvent: SkikoPointerEvent + get() { + val x = LOWORD(position) + val y = HIWORD(position) + var pressed = SkikoMouseButtons.NONE + if (pressedButtons and MK_LBUTTON.toULong() != 0uL) { + pressed += SkikoMouseButtons.LEFT + } + if (pressedButtons and MK_RBUTTON.toULong() != 0uL) { + pressed += SkikoMouseButtons.RIGHT + } + if (pressedButtons and MK_MBUTTON.toULong() != 0uL) { + pressed += SkikoMouseButtons.MIDDLE + } + + val deltaX: Double = when { + kind == SkikoPointerEventKind.SCROLL && inputModifiers.has(SkikoInputModifiers.SHIFT) -> HIWORD( + pressedButtons.toLong() + ).toShort().toDouble() / 120.0 + + else -> 0.0 + } + + val deltaY: Double = when { + kind == SkikoPointerEventKind.SCROLL && !inputModifiers.has(SkikoInputModifiers.SHIFT) -> HIWORD( + pressedButtons.toLong() + ).toShort().toDouble() / 120.0 + + else -> 0.0 + } + + return SkikoPointerEvent( + x = x.toDouble(), + y = y.toDouble(), + deltaX = deltaX, + deltaY = deltaY, + pressedButtons = pressed, + button = button, + kind = kind, + modifiers = inputModifiers, + platform = this + ) + } +} + +actual typealias SkikoTouchPlatformEvent = Any +actual typealias SkikoGesturePlatformEvent = Any diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/OsArch.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/OsArch.native.kt new file mode 100644 index 000000000..a2487f830 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/OsArch.native.kt @@ -0,0 +1,26 @@ +package org.jetbrains.skiko + +actual val hostOs: OS by lazy { + when (Platform.osFamily) { + OsFamily.MACOSX -> OS.MacOS + OsFamily.LINUX -> OS.Linux + OsFamily.WINDOWS -> OS.Windows + OsFamily.IOS -> OS.Ios + else -> throw Error("Unsupported OS ${Platform.osFamily}") + } +} + +actual val hostArch: Arch by lazy { + when (Platform.cpuArchitecture) { + CpuArchitecture.X64 -> Arch.X64 + CpuArchitecture.ARM64 -> Arch.Arm64 + else -> throw Error("Unsupported arch ${Platform.cpuArchitecture}") + } +} + +actual val hostId by lazy { + "${hostOs.id}-${hostArch.id}" +} + +actual val kotlinBackend: KotlinBackend + get() = KotlinBackend.Native \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Resources.native.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Resources.native.kt new file mode 100644 index 000000000..ae43160df --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/Resources.native.kt @@ -0,0 +1,47 @@ +package org.jetbrains.skiko + +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.toKString +import kotlinx.cinterop.usePinned +import platform.posix.* + +actual suspend fun loadBytesFromPath(path: String): ByteArray { + val file = fopen(path, "r") ?: run { + val error = strerror(errno)?.toKString() ?: "Unknown error" + throw Error("Can not open file '$path': $error") + } + + val size = file.let { + fseek(it, 0, SEEK_END) + val size = ftell(it) + fseek(it, 0, SEEK_SET) + size + } + + if (size < 0) { + fclose(file) + throw Error("Can not read file '$path'") + } + + if (size > Int.MAX_VALUE.toLong()) { + fclose(file) + throw Error("File '$path' is too long") + } + + if (size == 0) { + fclose(file) + return byteArrayOf() + } + + val bytes = ByteArray(size) + val result = bytes.usePinned { + fread(it.addressOf(0), 1, size.toULong(), file) + } + fclose(file) + + if (result != size.toULong()) { + throw Error("Can not read file '$path'") + } + + return bytes +} diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/SkiaLayer.mingw.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/SkiaLayer.mingw.kt new file mode 100644 index 000000000..024ae5be0 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/SkiaLayer.mingw.kt @@ -0,0 +1,230 @@ +package org.jetbrains.skiko + +import org.jetbrains.skia.Canvas +import org.jetbrains.skia.PictureRecorder +import org.jetbrains.skia.Rect +import org.jetbrains.skiko.redrawer.Redrawer +import org.jetbrains.skiko.redrawer.WindowsOpenGLRedrawer +import platform.windows.* +import kotlin.system.getTimeNanos + + +@SymbolName("GetDpiForWindow") +private external fun GetDpiForWindow(hwnd: HWND): UInt + +actual open class SkiaLayer { + + actual var renderApi: GraphicsApi = GraphicsApi.OPENGL + set(value) { + if (value != GraphicsApi.OPENGL) { + throw IllegalArgumentException("Only OpenGL is supported in Windows at the moment") + } + field = value + } + + actual val contentScale: Float + get() = GetDpiForWindow(window).toFloat() / 96.0f + + actual var fullscreen: Boolean + get() = false + set(value) { + if (value) throw IllegalArgumentException("Fullscreen is not supported in Windows at the moment") + } + + actual var transparency: Boolean + get() = false + set(value) { + if (value) throw IllegalArgumentException("Transparency is not supported in Windows at the moment") + } + + internal lateinit var window: HWND + + actual val component: Any? + get() = this.window + + actual var skikoView: SkikoView? = null + + private var redrawer: Redrawer? = null + + private var picture: PictureHolder? = null + private val pictureRecorder = PictureRecorder() + + internal var onWMPaint: (nanoTime: Long) -> Unit = {} + + internal var size: Pair = 0 to 0 + + fun isShowing(): Boolean { + return true + } + + actual fun attachTo(container: Any) { + attachTo(container as HWND) + } + + fun attachTo(window: HWND) { + this.window = window + redrawer = WindowsOpenGLRedrawer(this).apply { + syncSize() + needRedraw() + } + } + + actual fun detach() { + redrawer?.dispose() + redrawer = null + } + + actual fun needRedraw() { + redrawer?.needRedraw() + } + + private fun syncSize() { + redrawer?.syncSize() + redrawer?.needRedraw() + } + + internal fun update(nanoTime: Long) { + val (width, height) = size + val bounds = Rect.makeWH(width.toFloat(), height.toFloat()) + val canvas = pictureRecorder.beginRecording(bounds) + skikoView?.onRender(canvas, width, height, nanoTime) + + val picture = pictureRecorder.finishRecordingAsPicture() + this.picture = PictureHolder(picture, width, height) + } + + internal actual fun draw(canvas: Canvas) { + picture?.also { + canvas.drawPicture(it.instance) + } + } + + private fun getInputModifiers(): SkikoInputModifiers { + var mod = SkikoInputModifiers.EMPTY + if (GetAsyncKeyState(VK_SHIFT) < 0.toShort()) { + mod += SkikoInputModifiers.SHIFT + } + if (GetAsyncKeyState(VK_CONTROL) < 0.toShort()) { + mod += SkikoInputModifiers.CONTROL + } + if (GetAsyncKeyState(VK_MENU) < 0.toShort()) { + mod += SkikoInputModifiers.ALT + } + return mod + } + + fun windowProc(hwnd: HWND?, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT { + when (msg.toInt()) { + + WM_SIZE -> syncSize() + WM_PAINT -> onWMPaint(getTimeNanos()) + + WM_KEYDOWN -> skikoView?.onKeyboardEvent( + SkikoPlatformKeyboardEvent( + kind = SkikoKeyboardEventKind.DOWN, + virtualKey = wParam, + modifiers = lParam, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_KEYUP -> skikoView?.onKeyboardEvent( + SkikoPlatformKeyboardEvent( + kind = SkikoKeyboardEventKind.UP, + virtualKey = wParam, + modifiers = lParam, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_CHAR -> skikoView?.onInputEvent( + SkikoPlatformInputEvent( + charCode = wParam, + modifiers = lParam, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_MOUSEMOVE -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.MOVE, + pressedButtons = wParam, + position = lParam, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_LBUTTONDOWN -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.DOWN, + pressedButtons = wParam, + position = lParam, + button = SkikoMouseButtons.LEFT, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_LBUTTONUP -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.UP, + pressedButtons = wParam, + position = lParam, + button = SkikoMouseButtons.LEFT, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_RBUTTONDOWN -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.DOWN, + pressedButtons = wParam, + position = lParam, + button = SkikoMouseButtons.RIGHT, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_RBUTTONUP -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.UP, + pressedButtons = wParam, + position = lParam, + button = SkikoMouseButtons.RIGHT, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_MBUTTONDOWN -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.DOWN, + pressedButtons = wParam, + position = lParam, + button = SkikoMouseButtons.MIDDLE, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_MBUTTONUP -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.UP, + pressedButtons = wParam, + position = lParam, + button = SkikoMouseButtons.MIDDLE, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + WM_MOUSEWHEEL -> skikoView?.onPointerEvent( + SkikoPlatformPointerEvent( + kind = SkikoPointerEventKind.SCROLL, + pressedButtons = wParam, + position = lParam, + inputModifiers = getInputModifiers(), + ).skikoEvent + ) + + else -> return DefWindowProcW(hwnd, msg, wParam, lParam) + } + return 0 + } +} \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/SkikoKey.mingw.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/SkikoKey.mingw.kt new file mode 100644 index 000000000..34e779e76 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/SkikoKey.mingw.kt @@ -0,0 +1,130 @@ +package org.jetbrains.skiko + +actual enum class SkikoKey(actual val platformKeyCode: Int) { + KEY_UNKNOWN(-1), + + KEY_0(0x30), + KEY_1(0x31), + KEY_2(0x32), + KEY_3(0x33), + KEY_4(0x34), + KEY_5(0x35), + KEY_6(0x36), + KEY_7(0x37), + KEY_8(0x38), + KEY_9(0x39), + + KEY_A(0x41), + KEY_B(0x42), + KEY_C(0x43), + KEY_D(0x44), + KEY_E(0x45), + KEY_F(0x46), + KEY_G(0x47), + KEY_H(0x48), + KEY_I(0x49), + KEY_J(0x4A), + KEY_K(0x4B), + KEY_L(0x4C), + KEY_M(0x4D), + KEY_N(0x4E), + KEY_O(0x4F), + KEY_P(0x50), + KEY_Q(0x51), + KEY_R(0x52), + KEY_S(0x53), + KEY_T(0x54), + KEY_U(0x55), + KEY_V(0x56), + KEY_W(0x57), + KEY_X(0x58), + KEY_Y(0x59), + KEY_Z(0x5A), + + KEY_CLOSE_BRACKET(0xDD), + KEY_OPEN_BRACKET(0xDB), + KEY_QUOTE(0xDE), + KEY_SEMICOLON( 0xBA), + KEY_SLASH(0xBF), + KEY_COMMA(0xBC), + KEY_BACKSLASH( 0xDC), + KEY_PERIOD(0xBE), + KEY_BACK_QUOTE(0xC0), + KEY_EQUALS(0xBB), + KEY_MINUS(0xBD), + KEY_ENTER(0x0D), + KEY_ESCAPE(0x1B), + KEY_TAB(0x09), + KEY_BACKSPACE(0x08), + KEY_SPACE(0x20), + KEY_CAPSLOCK(0x14), + + KEY_LEFT_META(0x5B), + KEY_LEFT_SHIFT(0xA0), + KEY_LEFT_ALT(0xA4), + KEY_LEFT_CONTROL(0xA2), + + KEY_RIGHT_META(0x5C), + KEY_RIGHT_SHIFT(0xA1), + KEY_RIGHT_ALT(0xA5), + KEY_RIGHT_CONTROL(0xA3), + + KEY_MENU(0x5D), + + KEY_UP(0x26), + KEY_DOWN(0x28), + KEY_LEFT(0x25), + KEY_RIGHT(0x27), + + KEY_F1(0x70), + KEY_F2(0x71), + KEY_F3(0x72), + KEY_F4(0x73), + KEY_F5(0x74), + KEY_F6(0x75), + KEY_F7(0x76), + KEY_F8(0x77), + KEY_F9(0x78), + KEY_F10(0x79), + KEY_F11(0x7A), + KEY_F12(0x7B), + + KEY_PRINTSCEEN(0x2C), + KEY_SCROLL_LOCK(0x91), + KEY_PAUSE(0x13), + + KEY_INSERT(0x2D), + KEY_HOME(0x24), + KEY_PGUP(0x21), + KEY_DELETE(0x2E), + KEY_END(0x23), + KEY_PGDOWN(0x22), + + KEY_NUM_LOCK(0x90), + + KEY_NUMPAD_0(0x60), + KEY_NUMPAD_1(0x61), + KEY_NUMPAD_2(0x62), + KEY_NUMPAD_3(0x63), + KEY_NUMPAD_4(0x64), + KEY_NUMPAD_5(0x65), + KEY_NUMPAD_6(0x66), + KEY_NUMPAD_7(0x67), + KEY_NUMPAD_8(0x68), + KEY_NUMPAD_9(0x69), + + KEY_NUMPAD_ENTER(0x0D), + KEY_NUMPAD_ADD(0x6B), + KEY_NUMPAD_SUBTRACT(0x6D), + KEY_NUMPAD_MULTIPLY(0x6A), + KEY_NUMPAD_DIVIDE(0x6F), + KEY_NUMPAD_DECIMAL(0x6E); + + companion object { + + private val reverseMap = values().associateBy(SkikoKey::platformKeyCode) + fun valueOf(platformKeyCode: Int): SkikoKey { + return reverseMap[platformKeyCode] ?: KEY_UNKNOWN + } + } +} diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/context/WindowsOpenGLContextHandler.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/context/WindowsOpenGLContextHandler.kt new file mode 100644 index 000000000..58c06a74b --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/context/WindowsOpenGLContextHandler.kt @@ -0,0 +1,71 @@ +package org.jetbrains.skiko.context + +import kotlinx.cinterop.* +import org.jetbrains.skia.* +import org.jetbrains.skiko.RenderException +import org.jetbrains.skiko.SkiaLayer +import platform.opengl32.GL_DRAW_FRAMEBUFFER_BINDING +import platform.opengl32.GLenum +import platform.opengl32.glGetIntegerv + +internal class WindowsOpenGLContextHandler(layer: SkiaLayer) : ContextHandler(layer, layer::draw) { + + override fun initContext(): Boolean { + try { + if (context == null) { + context = DirectContext.makeGL() + } + } catch (e: Exception) { + println("Failed to create Skia OpenGL context!") + return false + } + return true + } + + private fun openglGetIntegerv(pname: GLenum): UInt { + var result: UInt = 0U + memScoped { + val data = alloc() + glGetIntegerv(pname, data.ptr) + result = data.value.toUInt() + } + return result + } + + private var currentWidth = 0 + private var currentHeight = 0 + + private fun isSizeChanged(width: Int, height: Int): Boolean { + if (width != currentWidth || height != currentHeight) { + currentWidth = width + currentHeight = height + return true + } + return false + } + + override fun initCanvas() { + val (width, height) = layer.size + if (isSizeChanged(width, height)) { + val fbId = openglGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING.toUInt()) + renderTarget = BackendRenderTarget.makeGL( + width, + height, + 0, + 8, + fbId.toInt(), + FramebufferFormat.GR_GL_RGBA8 + ) + surface = Surface.makeFromBackendRenderTarget( + context!!, + renderTarget!!, + SurfaceOrigin.BOTTOM_LEFT, + SurfaceColorFormat.RGBA_8888, + ColorSpace.sRGB + ) ?: throw RenderException("Cannot create surface") + + canvas = surface?.canvas + ?: error("Could not obtain Canvas from Surface") + } + } +} \ No newline at end of file diff --git a/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/redrawer/OpenGLRedrawer.mingw.kt b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/redrawer/OpenGLRedrawer.mingw.kt new file mode 100644 index 000000000..e8b00c111 --- /dev/null +++ b/skiko/src/mingwMain/kotlin/org/jetbrains/skiko/redrawer/OpenGLRedrawer.mingw.kt @@ -0,0 +1,84 @@ +package org.jetbrains.skiko.redrawer + +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.sizeOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.newSingleThreadContext +import org.jetbrains.skiko.FrameDispatcher +import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.context.WindowsOpenGLContextHandler +import platform.windows.* + +internal class WindowsOpenGLRedrawer( + private val skiaLayer: SkiaLayer +) : Redrawer { + + private val contextHandler = WindowsOpenGLContextHandler(skiaLayer) + + private val dc: HDC = GetDC(skiaLayer.window) ?: throw Error("Failed to get DC") + private val context: HGLRC + + override val renderInfo: String + get() = contextHandler.rendererInfo() + + init { + memScoped { + val descriptor = alloc().apply { + nSize = sizeOf().toUShort() + nVersion = 1u + dwFlags = + (PFD_DRAW_TO_WINDOW or PFD_DRAW_TO_BITMAP or PFD_SUPPORT_OPENGL or PFD_GENERIC_ACCELERATED or PFD_DOUBLEBUFFER or PFD_SWAP_LAYER_BUFFERS).toUInt() + iPixelType = PFD_TYPE_RGBA.toUByte() + cColorBits = 32u + cRedBits = 8u + cGreenBits = 8u + cBlueBits = 8u + cAlphaBits = 8u + cDepthBits = 32u + cStencilBits = 8u + } + val pixelFormat = ChoosePixelFormat(dc, descriptor.ptr) + SetPixelFormat(dc, pixelFormat, descriptor.ptr) + } + context = wglCreateContext(dc) ?: throw Error("Failed to create context") + wglMakeCurrent(dc, context) + skiaLayer.onWMPaint = { nanoTime -> + skiaLayer.update(nanoTime) + contextHandler.draw() + SwapBuffers(dc) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val frameDispatcher = FrameDispatcher(newSingleThreadContext("skiko-opengl-redrawer-dispatcher")) { + redrawImmediately() + } + + override fun dispose() { + contextHandler.dispose() + wglDeleteContext(context) + ReleaseDC(skiaLayer.window, dc) + skiaLayer.onWMPaint = {} + } + + override fun syncSize() { + memScoped { + val rect = alloc() + GetClientRect(skiaLayer.window, rect.ptr) + val width = rect.right - rect.left + val height = rect.bottom - rect.top + skiaLayer.size = width to height + } + } + + override fun needRedraw() { + frameDispatcher.scheduleFrame() + } + + override fun redrawImmediately() { + SendMessageA(skiaLayer.window, WM_PAINT, 0, 0) + + } +} \ No newline at end of file