Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames

https://github.com/leonbloy/pngj

### Compose bugs
- Can't use Lazy layouts
- It will crash once for no reason. If the error is caught and UI is restarted, it'll continue working fine.
- LocalDensity.current will cause crashes
-


### Design choices
`withLock` implementation found in `UpdateQueue` and `MapAreaAccess` -
These are operations that can be accessed from multiple threads but none of them are crucial do be finished quickly.
Expand Down
49 changes: 47 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ plugins {
kotlin("jvm") version "2.0.0"
id ("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
id("com.github.johnrengelman.shadow") version "8.1.1"
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0"
}

loom {
Expand All @@ -47,10 +49,12 @@ version = getCurrentVersion()
group = maven_group

repositories {
google()
mavenCentral()
maven(url = "https://repo.maven.apache.org/maven2/")
maven(url = "https://maven.shedaniel.me/") //TODO use maven local or multi project build to build from sources
maven(url = "https://maven.terraformersmc.com") //TODO use maven local or multi project build to build from sources
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

//configurations {
Expand All @@ -77,7 +81,7 @@ dependencies {
}
modCompileOnlyApi("com.terraformersmc", "modmenu", "8.0.0")

val ktorVersion = "2.3.5"
val ktorVersion = "2.3.11"
extraLibs(implementation("io.ktor", "ktor-server-core-jvm", ktorVersion))
extraLibs(implementation("io.ktor", "ktor-server-cio-jvm", ktorVersion))
extraLibs(implementation("io.ktor", "ktor-server-content-negotiation", ktorVersion))
Expand All @@ -86,7 +90,42 @@ dependencies {
extraLibs(implementation("io.ktor", "ktor-server-cors", ktorVersion))

extraLibs(implementation("org.tukaani", "xz", "1.9"))
extraLibs(implementation("com.akuleshov7", "ktoml-core", "0.5.0"))
extraLibs(implementation("com.akuleshov7", "ktoml-core", "0.5.1"))

extraLibs("org.jetbrains.skiko:skiko:0.8.4") {
attributes {
// attribute(Attribute.of("org.jetbrains.compose.ui", String::class.java), "desktop")
// attribute(Attribute.of("org.jetbrains.compose.ui", String::class.java), "awtRuntimeElements-published")
// attribute(Attribute.of("org.jetbrains.compose.ui", String::class.java), "awt")
// attribute(Attribute.of("org.gradle.libraryelements", String::class.java), LibraryElements.JAR)
// attribute(Attribute.of("org.gradle.usage", String::class.java), Usage.JAVA_RUNTIME)
// attribute(Attribute.of("org.jetbrains.kotlin.platform.type", String::class.java), "jvm")

attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.LIBRARY))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, LibraryElements.JAR))
attribute(Attribute.of("org.jetbrains.kotlin.platform.type", String::class.java), "jvm")
attribute(Attribute.of("ui", String::class.java), "awt")
}
}

val composeBom = project.dependencies.platform("androidx.compose:compose-bom:2024.05.00")
// implementation(composeBom)
// implementation(compose.desktop.currentOs) //TODO make builds for different OSes compose.desktop.common
// implementation(compose.desktop.common)
// implementation(compose.material)
extraLibs(implementation(composeBom)!!)
// extraLibs(implementation(compose.desktop.currentOs)!!) //TODO make builds for different OSes compose.desktop.common
extraLibs(implementation(compose.desktop.common)!!)
extraLibs(implementation(compose.desktop.macos_arm64)!!)
extraLibs(implementation(compose.desktop.macos_x64)!!)
extraLibs(implementation(compose.desktop.windows_x64)!!)
extraLibs(implementation(compose.desktop.linux_x64)!!)
extraLibs(implementation(compose.desktop.linux_arm64)!!)
extraLibs(implementation(compose.material)!!)
// implementation("dev.reformator.stacktracedecoroutinator:stacktrace-decoroutinator-jvm:2.3.9")
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.0")

// extraLibs(implementation("org.ojalgo", "ojalgo", "53.0.0"))
// extraLibs(implementation("ai.hypergraph", "kotlingrad", "0.4.7"))
// extraLibs(implementation("ar.com.hjg", "pngj", "2.1.0"))
Expand All @@ -98,6 +137,12 @@ dependencies {
implementation(kotlin("stdlib-jdk8"))
}

configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("org.jetbrains.skiko:skiko")).using(module("org.jetbrains.skiko:skiko-awt:0.8.4"))
}
}

@Suppress("UnstableApiUsage")
tasks.getByName<ProcessResources>("processResources") {
filesMatching("fabric.mod.json") {
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ fabric_version=0.99.4+1.20.6
maven_group = dev.wefhy
archives_base_name = whymap
mod_id = WhyMap

compose.version=1.6.7
7 changes: 5 additions & 2 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
pluginManagement {
repositories {
// google()
maven(url = "https://maven.fabricmc.net/") {
name = "Fabric"
}
mavenCentral()
gradlePluginPortal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

// plugins {
plugins {
// id 'fabric-loom' version loom_version
// id "org.jetbrains.kotlin.jvm" version kotlin_version
// }
id("org.jetbrains.compose").version("1.6.10")
}
}
5 changes: 3 additions & 2 deletions src/main/java/dev/wefhy/whymap/WhyMapClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package dev.wefhy.whymap

import com.mojang.blaze3d.systems.RenderSystem
import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld
import dev.wefhy.whymap.clothconfig.ConfigEntryPoint.getConfigScreen
import dev.wefhy.whymap.compose.ui.ConfigScreen
import dev.wefhy.whymap.config.UserSettings.MinimapPosition
import dev.wefhy.whymap.config.WhyUserSettings
import dev.wefhy.whymap.events.FeatureUpdateQueue
Expand Down Expand Up @@ -249,7 +249,8 @@ class WhyMapClient : ClientModInitializer {
WhyUserSettings.mapSettings.minimapMode = WhyUserSettings.mapSettings.minimapMode.next()
}
if (kbModSettings.wasPressed()) {
mc.setScreen(getConfigScreen(null))
// mc.setScreen(getConfigScreen(null))
mc.setScreen(ConfigScreen())
}

//TODO https://discord.com/channels/507304429255393322/807617488313516032/895854464060227665
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/dev/wefhy/whymap/WhyMapMod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class WhyMapMod : ModInitializer {
val tmpWorld = activeWorld
activeWorld = null
tmpWorld?.close()
RegionUpdateQueue.reset()
RegionUpdateQueue.reset() //TODO should I reset all queues?
LOGGER.info("Saved all data")
WorldEventQueue.addUpdate(WorldEventQueue.WorldEvent.LeaveWorld)
}
Expand Down
208 changes: 208 additions & 0 deletions src/main/java/dev/wefhy/whymap/compose/ComposeView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright (c) 2024 wefhy

package dev.wefhy.whymap.compose

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.scene.MultiLayerComposeScene
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import dev.wefhy.whymap.utils.Accessors.clientWindow
import dev.wefhy.whymap.utils.WhyDispatchers
import dev.wefhy.whymap.utils.WhyDispatchers.launchOnMain
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import net.minecraft.client.gui.DrawContext
import org.jetbrains.skiko.MainUIDispatcher
import java.awt.Component
import java.io.Closeable
import java.util.concurrent.Executors
import java.awt.event.KeyEvent as AwtKeyEvent

@OptIn(InternalComposeUiApi::class, ExperimentalComposeUiApi::class)
open class ComposeView(
nativeWidth: Int,
nativeHeight: Int,
private val density: Density,
private val content: @Composable () -> Unit
) : Closeable {
@OptIn(ExperimentalCoroutinesApi::class)
private val rawSingleThreadDispatcher = MainUIDispatcher.limitedParallelism(1)
protected val singleThreadDispatcher = rawSingleThreadDispatcher +
CoroutineExceptionHandler { _, throwable -> println(throwable) }
private var invalidated = true
// private val screenScale = 2 //TODO This is Macbook specific
private var nativeWidth by mutableStateOf(nativeWidth)
private var nativeHeight by mutableStateOf(nativeHeight)
private val coroutineContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val directRenderer: Renderer = DirectRenderer(nativeWidth, nativeHeight)
private val boxedContent: @Composable () -> Unit
get() = {
val(dpWidth, dpHeight) = with(LocalDensity.current) {
nativeWidth.toDp() to nativeHeight.toDp()
}
Box(Modifier.size(dpWidth, dpHeight)) {
content()
}
}

//TODO use ImageComposeScene, seems more popular?
private val scene = MultiLayerComposeScene(coroutineContext = WhyDispatchers.MainDispatcher, density = density) {
// private val scene = MultiLayerComposeScene(coroutineContext = singleThreadDispatcher, density = density) {
// private val scene = MultiLayerComposeScene(coroutineContext = coroutineContext, density = density) {
// private val scene = SingleLayerComposeScene(coroutineContext = coroutineContext, density = density) {
invalidated = true
}

init {
scene.setContent(boxedContent)
}

private inline fun onComposeThread(crossinline block: () -> Unit) = launchOnMain {
block()
}

private fun Offset.toComposeCoords(): Offset {
return this * clientWindow.scaleFactor.toFloat()
}

fun passLMBClick(x: Float, y: Float) = onComposeThread {
scene.sendPointerEvent(
eventType = PointerEventType.Press,
Offset(x, y).toComposeCoords(),
)
}

fun passMouseMove(x: Float, y: Float) = onComposeThread {
scene.sendPointerEvent(
eventType = PointerEventType.Move,
Offset(x, y).toComposeCoords(),
)
}

fun passLMBRelease(x: Float, y: Float) = onComposeThread {
scene.sendPointerEvent(
eventType = PointerEventType.Release,
Offset(x, y).toComposeCoords()
)
}

fun passScroll(x: Float, y: Float, scrollX: Float, scrollY: Float) = onComposeThread {
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
Offset(x, y).toComposeCoords(),
scrollDelta = Offset(scrollX, scrollY),
)
}

// private fun getAwtKeyEvent(key: Int, action: Int, modifiers: Int): KeyEvent {
// val k = Key(key)
// return java.awt.event.KeyEvent(
// scene,
// action,
// System.currentTimeMillis(),
// modifiers,
// key,
// k.toString().first()
// ).let {
// KeyEvent(k, KeyEventType.KeyDown, codePoint = key)
// }
// }
val dummy = object : Component() {}

private fun createKeyEvent(awtId: Int, time: Long, awtMods: Int, key: Int, char: Char, location: Int) = KeyEvent(
AwtKeyEvent(dummy, awtId, time, awtMods, key, char, location)
)

private fun remapKeycode(key: Int, char: Char): Int {
return when (key) {
0x0 -> char.toInt()
else -> key
}
}


fun passKeyPress(key: Int, action: Int, modifiers: Int) = onComposeThread {
// scene.sendKeyEvent(androidx.compose.ui.input.key.KeyEvent(AwtKeyEvent.KEY_TYPED, System.nanoTime() / 1_000_000, getAwtMods(), remapKeycode(key, char), 0.toChar(), AwtKeyEvent.KEY_LOCATION_STANDARD))
// scene.sendKeyEvent(KeyEvent(AwtKeyEvent.KEY_TYPED, System.nanoTime() / 1_000_000, getAwtMods(), remapKeycode(key, char), 0.toChar(), AwtKeyEvent.KEY_LOCATION_STANDARD))
val time = System.nanoTime() / 1_000_000
val kmod = modifiers//getAwtMods()
val char = Key(key).toString().first()
val native1 = createKeyEvent(AwtKeyEvent.KEY_PRESSED, time, kmod, remapKeycode(key, char), 0.toChar(), AwtKeyEvent.KEY_LOCATION_STANDARD)
val native2 = createKeyEvent(AwtKeyEvent.KEY_TYPED, time, kmod, 0, char, AwtKeyEvent.KEY_LOCATION_UNKNOWN)
// val k = Key(key)
// val event1 = KeyEvent(k, KeyEventType.KeyDown, codePoint = key, nativeEvent = native1)
// scene.sendKeyEvent(event1)
// val event2 = KeyEvent(k, KeyEventType.Unknown, codePoint = key, nativeEvent = native2)
// scene.sendKeyEvent(event2)
val event = KeyEvent(Key(key), KeyEventType.KeyDown, codePoint = key, isShiftPressed = (modifiers and 1) != 0, nativeEvent = native1)
scene.sendKeyEvent(event)//getAwtKeyEvent(key, action, modifiers)))
val event2 = KeyEvent(Key(key), KeyEventType.Unknown, codePoint = key, isShiftPressed = (modifiers and 1) != 0, nativeEvent = native2)
scene.sendKeyEvent(event2)//getAwtKeyEvent(key, action, modifiers)))
}

fun passKeyRelease(key: Int, action: Int, modifiers: Int) = onComposeThread {
val event = KeyEvent(Key(key), KeyEventType.KeyUp, codePoint = key)
scene.sendKeyEvent(event)//getAwtKeyEvent(key, action, modifiers)))
}

var isRendering = false

fun render(drawContext: DrawContext, tickDelta: Float) {
// println("Trying to start rendering on thread ${Thread.currentThread().name}!")
if (isRendering) throw Exception("Already rendering!")
isRendering = true
// if (!invalidated) return Unit.also {
// println("Cancelled rendering on thread ${Thread.currentThread().name}!")
// isRendering = false
// }
nativeWidth = clientWindow.framebufferWidth
nativeHeight = clientWindow.framebufferHeight
directRenderer.onSizeChange(
nativeWidth,
nativeHeight
)
directRenderer.render(drawContext, tickDelta) { glCanvas ->
/**
* So the problem is
* - scene.render needs to run on minecraft render thread
* - but it also needs to run on the same thread as the scene
* - scene under the hood probably uses `val MainUIDispatcher: CoroutineDispatcher get() = SwingDispatcher`
* For some reason, this only happens
*
* The problem is in GlobalSnapshotManager.ensureStarted - it uses swing thread to consume events
*/

try {
// println("Rendering START!")
scene.render(glCanvas, System.nanoTime())
// println("Rendered END!")
invalidated = false
} catch (e: Exception) {
e.printStackTrace()
scene.setContent(boxedContent)
}
}
// println("Finished rendering on thread ${Thread.currentThread().name}!")
isRendering = false
}

override fun close() {
directRenderer.close()
scene.close()
}
}
Loading