From a5e667173d766221a324d78150079abd5a0c340b Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Sat, 8 Jun 2024 11:05:01 +0200 Subject: [PATCH 01/23] Add compose dependencies --- build.gradle.kts | 42 ++++++++++++++++++++++++++++++++++++++++-- gradle.properties | 2 ++ settings.gradle.kts | 7 +++++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 596bdd0..d0c4193 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { @@ -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 { @@ -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)) @@ -86,7 +90,35 @@ 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.material)!!) + // 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")) @@ -98,6 +130,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") { filesMatching("fabric.mod.json") { diff --git a/gradle.properties b/gradle.properties index 0701097..abd127d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 535dde2..cd40a0c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") + } } From 902f1e7e957496319c93162a6391f610801ad673 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Sat, 8 Jun 2024 12:19:52 +0200 Subject: [PATCH 02/23] Display Compose --- build.gradle.kts | 5 + .../java/dev/wefhy/whymap/WhyMapClient.kt | 5 +- .../dev/wefhy/whymap/compose/ComposeView.kt | 119 ++++++++++++++ .../wefhy/whymap/compose/DirectRenderer.kt | 106 ++++++++++++ .../java/dev/wefhy/whymap/compose/Renderer.kt | 13 ++ .../wefhy/whymap/compose/ui/ConfigScreen.kt | 153 ++++++++++++++++++ .../java/dev/wefhy/whymap/utils/Accessors.kt | 10 ++ .../dev/wefhy/whymap/utils/WhyDispatchers.kt | 18 +++ 8 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/ComposeView.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/Renderer.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt create mode 100644 src/main/java/dev/wefhy/whymap/utils/Accessors.kt diff --git a/build.gradle.kts b/build.gradle.kts index d0c4193..f1cd18d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,6 +117,11 @@ dependencies { 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)!!) // extraLibs(implementation("org.ojalgo", "ojalgo", "53.0.0")) diff --git a/src/main/java/dev/wefhy/whymap/WhyMapClient.kt b/src/main/java/dev/wefhy/whymap/WhyMapClient.kt index 3141edc..36e0a82 100644 --- a/src/main/java/dev/wefhy/whymap/WhyMapClient.kt +++ b/src/main/java/dev/wefhy/whymap/WhyMapClient.kt @@ -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 @@ -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 diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt new file mode 100644 index 0000000..4680bdd --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -0,0 +1,119 @@ +// 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.pointer.PointerEventType +import androidx.compose.ui.scene.SingleLayerComposeScene +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.launchOnMain +import kotlinx.coroutines.asCoroutineDispatcher +import net.minecraft.client.gui.DrawContext +import java.io.Closeable +import java.util.concurrent.Executors + +@OptIn(InternalComposeUiApi::class, ExperimentalComposeUiApi::class) +open class ComposeView( + width: Int, + height: Int, + private val density: Density = Density(2f), + private val content: @Composable () -> Unit +) : Closeable { + private val screenScale = 2 //TODO This is Macbook specific + private var width by mutableStateOf(width) + private var height by mutableStateOf(height) + private val coroutineContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val directRenderer: Renderer = DirectRenderer(width * screenScale, height * screenScale) + private val boxedContent: @Composable () -> Unit + get() = { +// val width by ::width.asFlow().collectAsState(0) +// val height by ::height.asFlow().collectAsState(0) +// with(LocalDensity.current) { //TODO this causes the crash lol xD +// val dpWidth = outputWidth.toDp() +// val dpHeight = outputHeight.toDp() +// println("DP: $dpWidth, $dpHeight") +// Box(Modifier.size(dpWidth, dpHeight)) { +// Box(Modifier.size(dpWidth, dpHeight).background(Color(0x77000077.toInt()))) { + Box(Modifier.size(width.dp * screenScale / density.density, height.dp * screenScale / density.density)) { + content() + } +// } + } + + //TODO use ImageComposeScene, seems more popular? + private val scene = SingleLayerComposeScene(coroutineContext = coroutineContext, density = density) + + 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), + ) + } + + fun render(drawContext: DrawContext, tickDelta: Float) { + width = clientWindow.width + height = clientWindow.height + directRenderer.onSizeChange( + width * screenScale, + height * screenScale + ) + directRenderer.render(drawContext, tickDelta) { glCanvas -> + try { + scene.render(glCanvas, System.nanoTime()) + } catch (e: Exception) { + e.printStackTrace() + scene.setContent(boxedContent) + } + } + } + + override fun close() { + directRenderer.close() + scene.close() + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt b/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt new file mode 100644 index 0000000..733ea96 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt @@ -0,0 +1,106 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose + +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.asComposeCanvas +import com.mojang.blaze3d.systems.RenderSystem +import dev.wefhy.whymap.utils.WhyDispatchers.launchOnMain +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.BufferRenderer +import org.jetbrains.skia.* +import org.jetbrains.skia.FramebufferFormat.Companion.GR_GL_RGBA8 +import org.lwjgl.opengl.GL11.* +import org.lwjgl.opengl.GL13.GL_TEXTURE0 +import org.lwjgl.opengl.GL13.glActiveTexture +import org.lwjgl.opengl.GL14.GL_FUNC_ADD +import org.lwjgl.opengl.GL14.glBlendEquation +import org.lwjgl.opengl.GL30.GL_FRAMEBUFFER_BINDING +import org.lwjgl.opengl.GL33 + +internal class DirectRenderer(var width: Int, var height: Int) : Renderer() { + + private val context: DirectContext by lazy { DirectContext.makeGL() } + private var renderTarget: BackendRenderTarget? = null + private var surface: Surface? = null + private var composeCanvas: Canvas? = null + + init { + println("Init DirectRenderer with width: $width, height: $height") + System.setProperty("skiko.macos.opengl.enabled", "true") + } + + private fun initilaize() { + RenderSystem.assertOnRenderThread() + surface?.close() + renderTarget?.close() + renderTarget = BackendRenderTarget.makeGL(width, height, 0, 8, glGetInteger(GL_FRAMEBUFFER_BINDING), GR_GL_RGBA8).also { + surface = Surface.makeFromBackendRenderTarget( + context, it, SurfaceOrigin.BOTTOM_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB + ) + } + composeCanvas = surface?.canvas?.asComposeCanvas() + } + + override fun onSizeChange(width: Int, height: Int) { + if (this.width == width && this.height == height) return + println("DirectRenderer onSizeChange: $width, $height") + RenderSystem.assertOnRenderThread() + this.width = width + this.height = height + initilaize() + } + + override fun render(drawContext: DrawContext, tickDelta: Float, block: (Canvas) -> Unit) = launchOnMain { + RenderSystem.assertOnRenderThread() + enterManaged() + if (composeCanvas == null) { + initilaize() + } + block(composeCanvas!!) + surface!!.flush() + exitManaged() + context.resetAll() + } + + override fun invalidate() { + + } + + override fun close() { + surface?.close() + renderTarget?.close() + context.close() + } + + private fun enterManaged() { + RenderSystem.assertOnRenderThread() + RenderSystem.pixelStore(GL_UNPACK_ROW_LENGTH, 0) + RenderSystem.pixelStore(GL_UNPACK_SKIP_PIXELS, 0) + RenderSystem.pixelStore(GL_UNPACK_SKIP_ROWS, 0) + RenderSystem.pixelStore(GL_UNPACK_ALIGNMENT, 4) + } + + private fun exitManaged() { + RenderSystem.assertOnRenderThread() + BufferRenderer.reset() + GL33.glBindSampler(0, 0) + RenderSystem.disableBlend() + glDisable(GL_BLEND) + RenderSystem.blendFunc(GL_SRC_ALPHA, GL_ONE) + glBlendFunc(GL_SRC_ALPHA, GL_ONE) + RenderSystem.blendEquation(GL_FUNC_ADD) + glBlendEquation(GL_FUNC_ADD) + RenderSystem.colorMask(true, true, true, true) + glColorMask(true, true, true, true) + RenderSystem.depthMask(true) + glDepthMask(true) + RenderSystem.disableScissor() + glDisable(GL_SCISSOR_TEST) + glDisable(GL_STENCIL_TEST) + RenderSystem.disableDepthTest() + glDisable(GL_DEPTH_TEST) + glActiveTexture(GL_TEXTURE0) + RenderSystem.activeTexture(GL_TEXTURE0) + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/Renderer.kt b/src/main/java/dev/wefhy/whymap/compose/Renderer.kt new file mode 100644 index 0000000..ab72755 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/Renderer.kt @@ -0,0 +1,13 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose + +import androidx.compose.ui.graphics.Canvas +import net.minecraft.client.gui.DrawContext +import java.io.Closeable + +internal abstract class Renderer: Closeable { + abstract fun render(drawContext: DrawContext, tickDelta: Float, block: (Canvas) -> Unit) + abstract fun invalidate() + abstract fun onSizeChange(width: Int, height: Int) +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt new file mode 100644 index 0000000..081c9c8 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -0,0 +1,153 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandIn +import androidx.compose.animation.shrinkOut +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.Card +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import dev.wefhy.whymap.compose.ComposeView +import dev.wefhy.whymap.utils.Accessors.clientWindow +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import kotlin.random.Random + +class ConfigScreen : Screen(Text.of("Config")) { + + var i = 0 + val random = Random(0) + + private val composeView = ComposeView( + width = clientWindow.width, + height = clientWindow.height, + density = Density(3f) + ) { + UI() + } + + init { +// RenderThreadScope.launch { +// while (true) { +//// composeView.passLMBClick(148.8671875f, 43.724609375f) +// composeView.passLMBClick(191f, 90f) +// delay(random.nextLong(25, 50)) +//// composeView.passLMBRelease(148.8671875f, 43.724609375f) +// composeView.passLMBRelease(191f, 90f) +// delay(random.nextLong(50, 550)) +// } +// } + } + + @OptIn(ExperimentalComposeUiApi::class) + @Preview + @Composable + fun UI() { + var clicks by remember { mutableStateOf(0) } + var color by remember { mutableStateOf(Color.Green) } + var showList by remember { mutableStateOf(true) } + Card( + elevation = 20.dp, modifier = Modifier.padding(200.dp, 0.dp, 0.dp, 0.dp).padding(8.dp)/*.onPointerEvent(PointerEventType.Move) { + val position = it.changes.first().position + color = Color(position.x.toInt() % 256, position.y.toInt() % 256, 0) + }*/ + ) { + Row(Modifier.padding(8.dp)) { + println("Recomposition ${i++}") + Column { + Text("Clicks: $clicks") + Button(onClick = { clicks++ }) { + Text("Click me!") + color = Color(0x7F777700) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Show List") + Switch(checked = showList, onCheckedChange = { showList = it }) + } + } +// if(showList) { +// LazyColumn { //TODO scrolling LazyColumn will cause race condition in Recomposer, broadcastFrameClock +// items(20) { +// Text("Item $it") +// } +// } +// } + AnimatedVisibility( + showList, + enter = expandIn(), + exit = shrinkOut() + ) { + val rememberScrollState = rememberScrollState() +// Column(Modifier.scrollable(rememberScrollState, orientation = Orientation.Vertical)) { + Column(Modifier.verticalScroll(rememberScrollState)) { + for (it in 0..20) { + val hovered = remember { mutableStateOf(false) } + Text("Item $it", Modifier.background(if (hovered.value) Color.Gray else Color.Transparent).padding(8.dp).onPointerEvent( + PointerEventType.Move) { + hovered.value = true + }) + } + } + } + } + } + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { +// super.render(context, mouseX, mouseY, delta) + composeView.render(context, delta) + } + + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { +// println("Mouse clicked at $mouseX, $mouseY") + composeView.passLMBClick(mouseX.toFloat(), mouseY.toFloat()) + return super.mouseClicked(mouseX, mouseY, button) + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + composeView.passMouseMove(mouseX.toFloat(), mouseY.toFloat()) + super.mouseMoved(mouseX, mouseY) + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { +// println("Mouse released at $mouseX, $mouseY") + composeView.passLMBRelease(mouseX.toFloat(), mouseY.toFloat()) + return super.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { +// println("Mouse scrolled at $mouseX, $mouseY, $horizontalAmount, $verticalAmount") + composeView.passScroll(mouseX.toFloat(), mouseY.toFloat(), horizontalAmount.toFloat(), verticalAmount.toFloat()) + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + } + + override fun close() { + composeView.close() + super.close() + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/utils/Accessors.kt b/src/main/java/dev/wefhy/whymap/utils/Accessors.kt new file mode 100644 index 0000000..a80643e --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/utils/Accessors.kt @@ -0,0 +1,10 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.utils + +import net.minecraft.client.MinecraftClient + +object Accessors { + inline val clientInstance get() = MinecraftClient.getInstance() + inline val clientWindow get() = clientInstance.window +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/utils/WhyDispatchers.kt b/src/main/java/dev/wefhy/whymap/utils/WhyDispatchers.kt index 382d505..e8d5f65 100644 --- a/src/main/java/dev/wefhy/whymap/utils/WhyDispatchers.kt +++ b/src/main/java/dev/wefhy/whymap/utils/WhyDispatchers.kt @@ -2,7 +2,11 @@ package dev.wefhy.whymap.utils +import dev.wefhy.whymap.utils.Accessors.clientInstance +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import java.util.concurrent.* import java.util.concurrent.atomic.AtomicInteger import kotlin.math.max @@ -15,6 +19,20 @@ object WhyDispatchers { val Render = newReversePriorityFixedThreadPool(safeThreads).asCoroutineDispatcher() val Encoding = newReversePriorityFixedThreadPool(safeThreads).asCoroutineDispatcher() val LowPriority = Executors.newFixedThreadPool(safeThreads, LowPriorityThreadFactory).asCoroutineDispatcher() + val MainDispatcher by lazy { clientInstance.asCoroutineDispatcher() } + val MainScope by lazy { CoroutineScope(MainDispatcher) } + + fun launchOnMain(block: suspend () -> Unit) { + MainScope.launch { + block() + } + } + + fun blockOnMain(block: suspend () -> Unit) { + runBlocking(MainDispatcher) { + block() + } + } object LowPriorityThreadFactory : ThreadFactory { private const val priority = Thread.MIN_PRIORITY From 6ccc13e60433f07333c8cb2acdd865dc54a42f4c Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Sat, 8 Jun 2024 13:30:41 +0200 Subject: [PATCH 03/23] Rendering crashes --- .../dev/wefhy/whymap/compose/ComposeView.kt | 19 ++- .../wefhy/whymap/compose/DirectRenderer.kt | 4 +- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 41 +++-- .../wefhy/whymap/compose/ui/WaypointsView.kt | 145 ++++++++++++++++++ 4 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt index 4680bdd..1f07410 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -30,6 +30,7 @@ open class ComposeView( private val density: Density = Density(2f), private val content: @Composable () -> Unit ) : Closeable { + private var invalidated = true private val screenScale = 2 //TODO This is Macbook specific private var width by mutableStateOf(width) private var height by mutableStateOf(height) @@ -52,7 +53,9 @@ open class ComposeView( } //TODO use ImageComposeScene, seems more popular? - private val scene = SingleLayerComposeScene(coroutineContext = coroutineContext, density = density) + private val scene = SingleLayerComposeScene(coroutineContext = coroutineContext, density = density) { + invalidated = true + } init { scene.setContent(boxedContent) @@ -95,7 +98,16 @@ open class ComposeView( ) } + 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 + } width = clientWindow.width height = clientWindow.height directRenderer.onSizeChange( @@ -104,12 +116,17 @@ open class ComposeView( ) directRenderer.render(drawContext, tickDelta) { glCanvas -> 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() { diff --git a/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt b/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt index 733ea96..56dafb1 100644 --- a/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt +++ b/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt @@ -5,7 +5,7 @@ package dev.wefhy.whymap.compose import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.asComposeCanvas import com.mojang.blaze3d.systems.RenderSystem -import dev.wefhy.whymap.utils.WhyDispatchers.launchOnMain +import dev.wefhy.whymap.utils.WhyDispatchers.blockOnMain import net.minecraft.client.gui.DrawContext import net.minecraft.client.render.BufferRenderer import org.jetbrains.skia.* @@ -51,7 +51,7 @@ internal class DirectRenderer(var width: Int, var height: Int) : Renderer() { initilaize() } - override fun render(drawContext: DrawContext, tickDelta: Float, block: (Canvas) -> Unit) = launchOnMain { + override fun render(drawContext: DrawContext, tickDelta: Float, block: (Canvas) -> Unit) = blockOnMain { RenderSystem.assertOnRenderThread() enterManaged() if (composeCanvas == null) { diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 081c9c8..1d5714e 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandIn import androidx.compose.animation.shrinkOut import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -21,19 +20,23 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import dev.wefhy.whymap.WhyMapMod import dev.wefhy.whymap.compose.ComposeView import dev.wefhy.whymap.utils.Accessors.clientWindow import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text +import java.util.* import kotlin.random.Random class ConfigScreen : Screen(Text.of("Config")) { + companion object { + private var initializationCount = 0 + } + var i = 0 val random = Random(0) @@ -46,6 +49,7 @@ class ConfigScreen : Screen(Text.of("Config")) { } init { + println("ConfigScreen init ${++initializationCount}") // RenderThreadScope.launch { // while (true) { //// composeView.passLMBClick(148.8671875f, 43.724609375f) @@ -91,21 +95,38 @@ class ConfigScreen : Screen(Text.of("Config")) { // } // } // } +// return@Row AnimatedVisibility( showList, enter = expandIn(), exit = shrinkOut() ) { + val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() + val entries = waypoints.mapIndexed() { i, it -> + WaypointEntry( + name = it.name, distance = 0.0f, waypointId = i, date = Date(), waypointStatus = WaypointEntry.Status.NEW, waypointType = WaypointEntry.Type.SIGHTSEEING + ) + } + WaypointsView(entries) { + println("Refresh!") + } val rememberScrollState = rememberScrollState() // Column(Modifier.scrollable(rememberScrollState, orientation = Orientation.Vertical)) { Column(Modifier.verticalScroll(rememberScrollState)) { - for (it in 0..20) { - val hovered = remember { mutableStateOf(false) } - Text("Item $it", Modifier.background(if (hovered.value) Color.Gray else Color.Transparent).padding(8.dp).onPointerEvent( - PointerEventType.Move) { - hovered.value = true - }) - } +// for (it in 0..20) { +// val hovered = remember { mutableStateOf(false) } +// Text("Item $it", Modifier.background(if (hovered.value) Color.Gray else Color.Transparent).padding(8.dp).onPointerEvent( +// PointerEventType.Move) { +// hovered.value = true +// }) +// } + +// for (entry in entries) { +// WaypointEntryView(entry) +// } +// WaypointsView(entries) { +// println("Refresh!") +// } } } } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt new file mode 100644 index 0000000..7004cd0 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -0,0 +1,145 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.* + + +class WaypointEntry( + val name: String, + val distance: Float, + val waypointId: Int, + val date: Date, + val waypointStatus: Status, + val waypointType: Type +) { + enum class Status { + NEW, REACHED, ARCHIVED + } + + enum class Type { + SPAWN, DEATH, TODO, HOME, SIGHTSEEING + } +} + +@Composable +fun WaypointEntryView(waypointEntry: WaypointEntry) { + val dateFormatter = SimpleDateFormat("HH:mm, EEE, MMM d", Locale.getDefault()) + Card(modifier = Modifier.fillMaxWidth(), elevation = 8.dp) { + Box( + Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + Column(modifier = Modifier.padding(4.dp)) { + Row { + Text(text = waypointEntry.name, fontWeight = FontWeight.Bold, fontSize = 20.sp) + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = "${waypointEntry.distance}m", + modifier = Modifier.align(Alignment.CenterEnd), + fontStyle = FontStyle.Italic, + fontSize = 17.sp + ) + } + } + Text(text = dateFormatter.format(waypointEntry.date), fontSize = 16.sp) + Text(text = waypointEntry.waypointId.toString(), color = Color.Gray, fontSize = 14.sp) + } + Text( + text = "${waypointEntry.waypointStatus}/${waypointEntry.waypointType}", + modifier = Modifier + .align(Alignment.BottomEnd) + .clip(RoundedCornerShape(8.dp)) + .background( + Color.Blue + ) + .padding(4.dp), + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontSize = 15.sp + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun WaypointsView(waypoints: List, onRefresh: () -> Unit) { + val refreshScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + + fun refresh() = refreshScope.launch { + refreshing = true + onRefresh() + delay(100) + refreshing = false + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + + Box(Modifier.pullRefresh(state).clipToBounds()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp) + ) { + items(waypoints) { + WaypointEntryView(it) + } + } + + PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) + } +} + +val viewEntry = WaypointEntry( + name = "Hello", + distance = 123.57f, + waypointId = 2137, + date = Date(), + waypointStatus = WaypointEntry.Status.NEW, + waypointType = WaypointEntry.Type.TODO +) + + +@Preview +@Composable +fun Preview() { + WaypointEntryView( + viewEntry + ) +} + +@Preview +@Composable +fun Preview2() { + WaypointsView( + listOf(viewEntry, viewEntry, viewEntry) + ){} +} \ No newline at end of file From dda01a4452b02b9eb4fc3e3f142063651e08ccb8 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Sun, 9 Jun 2024 21:28:45 +0200 Subject: [PATCH 04/23] Map rendered --- .../dev/wefhy/whymap/compose/ComposeView.kt | 12 ++--- .../wefhy/whymap/compose/DirectRenderer.kt | 3 ++ .../wefhy/whymap/compose/ui/ConfigScreen.kt | 21 ++++++++ .../dev/wefhy/whymap/compose/ui/MapTile.kt | 40 +++++++++++++++ .../wefhy/whymap/compose/ui/WaypointsView.kt | 26 +++++++--- src/main/java/dev/wefhy/whymap/utils/Utils.kt | 1 + .../dev/wefhy/whymap/whygraphics/WhyColor.kt | 7 +++ .../dev/wefhy/whymap/whygraphics/WhyTile.kt | 50 +++++++++++++++++++ .../wefhy/whymap/whygraphics/WhyTiledImage.kt | 14 ++++++ 9 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt index 1f07410..c617071 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -101,13 +101,13 @@ open class ComposeView( var isRendering = false fun render(drawContext: DrawContext, tickDelta: Float) { - println("Trying to start rendering on thread ${Thread.currentThread().name}!") +// 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 - } +// if (!invalidated) return Unit.also { +// println("Cancelled rendering on thread ${Thread.currentThread().name}!") +// isRendering = false +// } width = clientWindow.width height = clientWindow.height directRenderer.onSizeChange( @@ -125,7 +125,7 @@ open class ComposeView( scene.setContent(boxedContent) } } - println("Finished rendering on thread ${Thread.currentThread().name}!") +// println("Finished rendering on thread ${Thread.currentThread().name}!") isRendering = false } diff --git a/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt b/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt index 56dafb1..ac3ee22 100644 --- a/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt +++ b/src/main/java/dev/wefhy/whymap/compose/DirectRenderer.kt @@ -20,6 +20,9 @@ import org.lwjgl.opengl.GL33 internal class DirectRenderer(var width: Int, var height: Int) : Renderer() { +// private val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Can't create MTLDevice") +// private val commandQueue = device.newCommandQueue() ?: throw IllegalStateException("Can't create MTLCommandQueue") +// private val context: DirectContext by lazy { DirectContext.makeMetal() } private val context: DirectContext by lazy { DirectContext.makeGL() } private var renderTarget: BackendRenderTarget? = null private var surface: Surface? = null diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 1d5714e..188fa5b 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod import dev.wefhy.whymap.compose.ComposeView import dev.wefhy.whymap.utils.Accessors.clientWindow +import dev.wefhy.whymap.utils.MapTile +import dev.wefhy.whymap.utils.TileZoom import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text @@ -69,6 +71,7 @@ class ConfigScreen : Screen(Text.of("Config")) { var clicks by remember { mutableStateOf(0) } var color by remember { mutableStateOf(Color.Green) } var showList by remember { mutableStateOf(true) } + var showMap by remember { mutableStateOf(false) } Card( elevation = 20.dp, modifier = Modifier.padding(200.dp, 0.dp, 0.dp, 0.dp).padding(8.dp)/*.onPointerEvent(PointerEventType.Move) { val position = it.changes.first().position @@ -87,6 +90,10 @@ class ConfigScreen : Screen(Text.of("Config")) { Text("Show List") Switch(checked = showList, onCheckedChange = { showList = it }) } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Show Map") + Switch(checked = showMap, onCheckedChange = { showMap = it }) + } } // if(showList) { // LazyColumn { //TODO scrolling LazyColumn will cause race condition in Recomposer, broadcastFrameClock @@ -96,6 +103,20 @@ class ConfigScreen : Screen(Text.of("Config")) { // } // } // return@Row + + AnimatedVisibility( + showMap, + enter = expandIn(), + exit = shrinkOut() + ) { + Column { + Text("Map") + MapTileView(MapTile(65532, 65543, TileZoom.RegionZoom).toLocalTile()) + } +// MapTileView(LocalTileThumbnail(16383, 16384, TileZoom.ThumbnailZoom)) + } + + AnimatedVisibility( showList, enter = expandIn(), diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt new file mode 100644 index 0000000..651fc73 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -0,0 +1,40 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld +import dev.wefhy.whymap.tiles.region.MapArea +import dev.wefhy.whymap.utils.LocalTileRegion +import dev.wefhy.whymap.whygraphics.WhyTiledImage + +@Composable +fun MapTileView(regionTile: LocalTileRegion) { + + var tile: MapArea? by remember { mutableStateOf(null) } + println("MapTileView recompose, tile: $tile, regionTile: $regionTile") + LaunchedEffect(regionTile) { + println("MapTileView LaunchedEffect") + activeWorld?.mapRegionManager?.getRegionForTilesRendering(regionTile) { + println("MapTileView LaunchedEffect getRegionForTilesRendering, tile: ${this@getRegionForTilesRendering}") + tile = this@getRegionForTilesRendering + } + } + + + Column { + Text("Tile $tile") + Canvas(modifier = Modifier.fillMaxSize()) { + println("MapTileView Canvas recompose, tile: $tile") + val t: WhyTiledImage? = tile?.renderWhyImageNow() +// drawRect(Color.Magenta, Offset(0f, 0f), Size(size.width, size.height)) + t?.drawTiledImage() + } + + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index 7004cd0..332cffa 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -5,9 +5,9 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text @@ -104,15 +104,27 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit) { val state = rememberPullRefreshState(refreshing, ::refresh) Box(Modifier.pullRefresh(state).clipToBounds()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), + val scrollState = rememberScrollState() + Column( + modifier = Modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp) +// contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp) ) { - items(waypoints) { - WaypointEntryView(it) + for (waypoint in waypoints) { + Box(Modifier.padding(8.dp)) { + WaypointEntryView(waypoint) + } } } +// LazyColumn( +// modifier = Modifier.fillMaxSize(), //TODO removing this causes race condition instead of crash +// verticalArrangement = Arrangement.spacedBy(8.dp), +// contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp) +// ) { +// items(waypoints) { +// WaypointEntryView(it) +// } +// } PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) } diff --git a/src/main/java/dev/wefhy/whymap/utils/Utils.kt b/src/main/java/dev/wefhy/whymap/utils/Utils.kt index 84bc524..dba59ba 100644 --- a/src/main/java/dev/wefhy/whymap/utils/Utils.kt +++ b/src/main/java/dev/wefhy/whymap/utils/Utils.kt @@ -30,6 +30,7 @@ const val _1_255 = 1f / 255 const val _1_3 = 1f / 3 const val _1_2 = 1f / 2 const val bestHashConst = 92821 +val rand = Random(0) inline fun Double.roundToString(places: Int) = String.format("%.${places}f", this) inline fun Float.roundToString(places: Int) = String.format("%.${places}f", this) diff --git a/src/main/java/dev/wefhy/whymap/whygraphics/WhyColor.kt b/src/main/java/dev/wefhy/whymap/whygraphics/WhyColor.kt index 4c0d1b1..73a9ee2 100644 --- a/src/main/java/dev/wefhy/whymap/whygraphics/WhyColor.kt +++ b/src/main/java/dev/wefhy/whymap/whygraphics/WhyColor.kt @@ -37,6 +37,13 @@ val FastWhyColor.intB val FastWhyColor.intARGB inline get() = (intA shl 24) or (intR shl 16) or (intG shl 8) or intB +val WhyColor.composeColor +// inline get() = androidx.compose.ui.graphics.Color(r, g, b, a) //TODO this causes crash due to negative red! How can it be negative? + inline get() = androidx.compose.ui.graphics.Color(intR, intG, intB, intA) + +val FastWhyColor.composeColor + inline get() = androidx.compose.ui.graphics.Color(r, g, b, a) + fun FastWhyColor.toWhyColor(): WhyColor = WhyColor(r, g, b, a) inline fun WhyColor.toFastWhyColor(): FastWhyColor = floatArrayOf(a, r, g, b) diff --git a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt index 64f3bb3..8a783d6 100644 --- a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt +++ b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt @@ -2,8 +2,17 @@ package dev.wefhy.whymap.whygraphics +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.unit.IntOffset import dev.wefhy.whymap.utils.ExpensiveCall +import dev.wefhy.whymap.utils.memoize +import dev.wefhy.whymap.utils.rand import net.minecraft.client.texture.NativeImage +import org.jetbrains.skia.ColorType +import org.jetbrains.skia.Image +import org.jetbrains.skia.ImageInfo import java.awt.image.BufferedImage import java.awt.image.WritableRaster @@ -42,6 +51,10 @@ open class WhyTile(val data: Array = Array(arraySize) { WhyColor.Trans ) } + val memoizedAverage = memoize { + average() + } + fun average(): WhyColor { var r = 0f @@ -74,6 +87,43 @@ open class WhyTile(val data: Array = Array(arraySize) { WhyColor.Trans ) } + context(DrawScope) + fun drawTile(xOffset: Int, yOffset: Int) { +// val image = toImageBitmap() + val image = memoizedImageBitmap(this) + println("Drawing $image at $xOffset, $yOffset of size ${image.width}, ${image.height}") +// drawRect(memoizedAverage(this).composeColor, Offset(xOffset.toFloat(), yOffset.toFloat()), Size(chunkSize.toFloat(), chunkSize.toFloat())) + if (rand.nextInt(4) == 1) { + drawImage(image, dstOffset = IntOffset(xOffset, yOffset)) + } + } + + val memoizedImageBitmap = memoize { + toImageBitmap() + } + + fun toImageBitmap(): ImageBitmap { //TODO cache + val bytes = ByteArray(arraySize shl 2) + for (i in 0 until arraySize) { + val color = data[i] + val index = i shl 2 + bytes[index] = color.intR.toByte() + bytes[index + 1] = color.intG.toByte() + bytes[index + 2] = color.intB.toByte() +// bytes[index + 3] = color.intA.toByte() + bytes[index + 3] = if (color.intA > 0) 0xFF.toByte() else 0.toByte() + } + val image = Image.makeRaster( //TODO this can get a NativePointer for maybe faster drawing + imageInfo = ImageInfo.makeN32Premul(width, height).withColorType(ColorType.RGBA_8888), + bytes = bytes, + rowBytes = width * 4 + ) + return image.toComposeImageBitmap().also { + println("Converted to ImageBitmap: " + + "${it.colorSpace}, ${it.width}, ${it.height}, ${it.config}") + } + } + fun writeInto(raster: WritableRaster, xOffset: Int, yOffset: Int) { var i = 0 // println("Writing tile at $xOffset, $yOffset") diff --git a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt index 8f13567..302c500 100644 --- a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt +++ b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt @@ -2,6 +2,7 @@ package dev.wefhy.whymap.whygraphics +import androidx.compose.ui.graphics.drawscope.DrawScope import dev.wefhy.whymap.config.WhyMapConfig import dev.wefhy.whymap.config.WhyMapConfig.blocksInChunkLog import dev.wefhy.whymap.utils.ExpensiveCall @@ -51,6 +52,19 @@ class WhyTiledImage( return image } + context(DrawScope) + fun drawTiledImage() { + println("Drawing $this WhyTiledImage") + for (y in 0 until yTiles) { + val line = data[y] + for (x in 0 until xTiles) { + val tile = line[x] ?: continue + println("Drawing tile at $x, $y") + tile.drawTile(x shl WhyTile.lineShl, y shl WhyTile.lineShl) + } + } + } + fun writeInto(raster: WritableRaster, offsetX: Int, offsetY: Int) { // println("Writing into raster at $offsetX, $offsetY") // raster.setPixel(offsetX, offsetY, intArrayOf(255, 0, 0)) From dac9cef6aa9de88ac131fa42ee4b94fd75426ae1 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Sun, 9 Jun 2024 23:12:21 +0200 Subject: [PATCH 05/23] Wrap canvas for better performance --- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 61 +++++++++++++++++-- .../dev/wefhy/whymap/whygraphics/WhyTile.kt | 15 ++--- .../wefhy/whymap/whygraphics/WhyTiledImage.kt | 6 +- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 651fc73..fc96d9e 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -4,15 +4,26 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text +import androidx.compose.foundation.layout.size import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.tiles.region.MapArea import dev.wefhy.whymap.utils.LocalTileRegion import dev.wefhy.whymap.whygraphics.WhyTiledImage + @Composable fun MapTileView(regionTile: LocalTileRegion) { @@ -28,13 +39,51 @@ fun MapTileView(regionTile: LocalTileRegion) { Column { - Text("Tile $tile") - Canvas(modifier = Modifier.fillMaxSize()) { +// Text("Tile $tile") + val t: WhyTiledImage? = tile?.renderWhyImageNow() + + val dpSize = with(LocalDensity.current) { + DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) + } + + + WrappedCanvas(modifier = Modifier.size(dpSize)) { size -> println("MapTileView Canvas recompose, tile: $tile") - val t: WhyTiledImage? = tile?.renderWhyImageNow() -// drawRect(Color.Magenta, Offset(0f, 0f), Size(size.width, size.height)) + drawRect(Rect(Offset(0f, 0f), Size(size.width, size.height)), Paint().apply { color = Color.Black }) t?.drawTiledImage() +// drawRect(Color.Magenta, Offset(0f, 0f), Size(size.width, size.height)) +// drawIntoCanvas { canvas -> +// with(canvas) { +// t?.drawTiledImage() +// } +// } } } -} \ No newline at end of file +} + +@Composable +fun CachedCanvas(modifier: Modifier = Modifier, block: DrawScope.() -> Unit) { + Spacer(modifier = Modifier.fillMaxSize().drawWithCache { + println("CachedCanvas recompose") + onDrawWithContent { + block() + } + }) +} + +@Composable +fun WrappedCanvas(modifier: Modifier = Modifier, block: Canvas.(Size) -> Unit) { + Canvas(modifier) { +// this.drawIntoCanvas { +// +// } + val image = ImageBitmap( + size.width.toInt(), + size.height.toInt(), + ImageBitmapConfig.Argb8888) + val canvas = Canvas(image) + canvas.block(size) + drawImage(image) + } +} diff --git a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt index 8a783d6..09add82 100644 --- a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt +++ b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt @@ -2,13 +2,14 @@ package dev.wefhy.whymap.whygraphics +import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.toOffset import dev.wefhy.whymap.utils.ExpensiveCall import dev.wefhy.whymap.utils.memoize -import dev.wefhy.whymap.utils.rand import net.minecraft.client.texture.NativeImage import org.jetbrains.skia.ColorType import org.jetbrains.skia.Image @@ -87,15 +88,15 @@ open class WhyTile(val data: Array = Array(arraySize) { WhyColor.Trans ) } - context(DrawScope) + context(Canvas) fun drawTile(xOffset: Int, yOffset: Int) { // val image = toImageBitmap() val image = memoizedImageBitmap(this) - println("Drawing $image at $xOffset, $yOffset of size ${image.width}, ${image.height}") +// println("Drawing $image at $xOffset, $yOffset of size ${image.width}, ${image.height}") // drawRect(memoizedAverage(this).composeColor, Offset(xOffset.toFloat(), yOffset.toFloat()), Size(chunkSize.toFloat(), chunkSize.toFloat())) - if (rand.nextInt(4) == 1) { - drawImage(image, dstOffset = IntOffset(xOffset, yOffset)) - } +// if (rand.nextInt(4) == 1) { + drawImage(image, topLeftOffset = IntOffset(xOffset, yOffset).toOffset(), paint = Paint()) +// } } val memoizedImageBitmap = memoize { diff --git a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt index 302c500..56664d4 100644 --- a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt +++ b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt @@ -2,7 +2,7 @@ package dev.wefhy.whymap.whygraphics -import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.Canvas import dev.wefhy.whymap.config.WhyMapConfig import dev.wefhy.whymap.config.WhyMapConfig.blocksInChunkLog import dev.wefhy.whymap.utils.ExpensiveCall @@ -52,14 +52,14 @@ class WhyTiledImage( return image } - context(DrawScope) + context(Canvas) fun drawTiledImage() { println("Drawing $this WhyTiledImage") for (y in 0 until yTiles) { val line = data[y] for (x in 0 until xTiles) { val tile = line[x] ?: continue - println("Drawing tile at $x, $y") +// println("Drawing tile at $x, $y") tile.drawTile(x shl WhyTile.lineShl, y shl WhyTile.lineShl) } } From 02f3ddaf3ce163b796d795345dc46e22dde47f88 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 01:00:59 +0200 Subject: [PATCH 06/23] Movable map --- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 61 ++++++++++++++----- .../wefhy/whymap/whygraphics/WhyTiledImage.kt | 13 ++++ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index fc96d9e..b879fa5 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -3,55 +3,86 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld -import dev.wefhy.whymap.tiles.region.MapArea import dev.wefhy.whymap.utils.LocalTileRegion -import dev.wefhy.whymap.whygraphics.WhyTiledImage +@OptIn(ExperimentalComposeUiApi::class) @Composable fun MapTileView(regionTile: LocalTileRegion) { + var offsetX by remember { mutableStateOf(0f) } + var offsetY by remember { mutableStateOf(0f) } - var tile: MapArea? by remember { mutableStateOf(null) } - println("MapTileView recompose, tile: $tile, regionTile: $regionTile") +// var tile: MapArea? by remember { mutableStateOf(null) } + var image: ImageBitmap? by remember { mutableStateOf(null) } + println("MapTileView recompose, image: $image, regionTile: $regionTile") LaunchedEffect(regionTile) { println("MapTileView LaunchedEffect") activeWorld?.mapRegionManager?.getRegionForTilesRendering(regionTile) { println("MapTileView LaunchedEffect getRegionForTilesRendering, tile: ${this@getRegionForTilesRendering}") - tile = this@getRegionForTilesRendering + val tile = this@getRegionForTilesRendering + image = tile.renderWhyImageNow().imageBitmap } } Column { // Text("Tile $tile") - val t: WhyTiledImage? = tile?.renderWhyImageNow() - +// val t: WhyTiledImage? = tile?.renderWhyImageNow() +// val image = t?.imageBitmap val dpSize = with(LocalDensity.current) { - DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) +// DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) + DpSize(image?.width?.toDp() ?: 1.dp, image?.height?.toDp() ?: 1.dp) } - WrappedCanvas(modifier = Modifier.size(dpSize)) { size -> - println("MapTileView Canvas recompose, tile: $tile") - drawRect(Rect(Offset(0f, 0f), Size(size.width, size.height)), Paint().apply { color = Color.Black }) - t?.drawTiledImage() -// drawRect(Color.Magenta, Offset(0f, 0f), Size(size.width, size.height)) +// WrappedCanvas(modifier = Modifier.size(dpSize)) { size -> + Canvas(modifier = Modifier.size(dpSize).clipToBounds().pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + offsetX += dragAmount.x + offsetY += dragAmount.y + } +// +// detectTransformGestures { centroid, pan, zoom, rotation -> +// println("Gesture: centroid: $centroid, pan: $pan, zoom: $zoom, rotation: $rotation") +// } + }.onPointerEvent(PointerEventType.Scroll) { + val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } + println(scrollDelta) + } + ) { + + println("MapTileView Canvas recompose, image: $image") +// drawRect(Rect(Offset(0f, 0f), Size(size.width, size.height)), Paint().apply { color = Color.Black }) +// t?.drawTiledImage() + drawRect(Color.Black, Offset(0f, 0f), Size(size.width, size.height)) + image?.let { im -> + drawImage(im, topLeft = Offset(offsetX, offsetY)) + } // drawIntoCanvas { canvas -> // with(canvas) { // t?.drawTiledImage() diff --git a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt index 56664d4..d13e9a0 100644 --- a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt +++ b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTiledImage.kt @@ -3,6 +3,8 @@ package dev.wefhy.whymap.whygraphics import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig import dev.wefhy.whymap.config.WhyMapConfig import dev.wefhy.whymap.config.WhyMapConfig.blocksInChunkLog import dev.wefhy.whymap.utils.ExpensiveCall @@ -65,6 +67,17 @@ class WhyTiledImage( } } + val imageBitmap by lazy { + val image = ImageBitmap( + width, + height, + ImageBitmapConfig.Argb8888) + Canvas(image).apply { + drawTiledImage() + } + image + } + fun writeInto(raster: WritableRaster, offsetX: Int, offsetY: Int) { // println("Writing into raster at $offsetX, $offsetY") // raster.setPixel(offsetX, offsetY, intArrayOf(255, 0, 0)) From 5870d23457a9bae96dd57abba62118bea7a8ee58 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 01:20:22 +0200 Subject: [PATCH 07/23] Manipulate map position with mouse --- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index b879fa5..694e41b 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -15,16 +15,15 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Canvas -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.utils.LocalTileRegion @@ -35,6 +34,10 @@ import dev.wefhy.whymap.utils.LocalTileRegion fun MapTileView(regionTile: LocalTileRegion) { var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } + var scale by remember { mutableStateOf(1f) } + val paint = Paint().apply { + filterQuality = FilterQuality.None + } // var tile: MapArea? by remember { mutableStateOf(null) } var image: ImageBitmap? by remember { mutableStateOf(null) } @@ -63,25 +66,23 @@ fun MapTileView(regionTile: LocalTileRegion) { Canvas(modifier = Modifier.size(dpSize).clipToBounds().pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() - offsetX += dragAmount.x - offsetY += dragAmount.y + offsetX += dragAmount.x / scale + offsetY += dragAmount.y / scale } -// -// detectTransformGestures { centroid, pan, zoom, rotation -> -// println("Gesture: centroid: $centroid, pan: $pan, zoom: $zoom, rotation: $rotation") -// } }.onPointerEvent(PointerEventType.Scroll) { val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } - println(scrollDelta) + scale *= 1 + scrollDelta.y / 10 } ) { - println("MapTileView Canvas recompose, image: $image") // drawRect(Rect(Offset(0f, 0f), Size(size.width, size.height)), Paint().apply { color = Color.Black }) // t?.drawTiledImage() drawRect(Color.Black, Offset(0f, 0f), Size(size.width, size.height)) image?.let { im -> - drawImage(im, topLeft = Offset(offsetX, offsetY)) + scale(scale) { +// drawImage(im, topLeft = Offset(offsetX, offsetY)) + drawImage(im, dstOffset = IntOffset(offsetX.toInt(), offsetY.toInt()), filterQuality = FilterQuality.None) + } } // drawIntoCanvas { canvas -> // with(canvas) { From 4729c1d6b1876557239420dbb35602821e5556ff Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 01:35:31 +0200 Subject: [PATCH 08/23] Swap tiles around --- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 694e41b..2be45f1 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -21,30 +21,37 @@ import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld +import dev.wefhy.whymap.utils.LocalTileBlock import dev.wefhy.whymap.utils.LocalTileRegion +import dev.wefhy.whymap.utils.TileZoom @OptIn(ExperimentalComposeUiApi::class) @Composable fun MapTileView(regionTile: LocalTileRegion) { + val nRequiredTiles = 3 + var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } var scale by remember { mutableStateOf(1f) } val paint = Paint().apply { filterQuality = FilterQuality.None } + val block = regionTile.getCenter() - LocalTileBlock(offsetX.toInt(), offsetY.toInt(), TileZoom.BlockZoom) + val centerTile = block.parent(TileZoom.RegionZoom) + val drawOffset = centerTile.getCenter() - regionTile.getCenter() // var tile: MapArea? by remember { mutableStateOf(null) } var image: ImageBitmap? by remember { mutableStateOf(null) } - println("MapTileView recompose, image: $image, regionTile: $regionTile") - LaunchedEffect(regionTile) { + println("MapTileView recompose, image: $image, regionTile: $centerTile") + LaunchedEffect(centerTile) { + image = null println("MapTileView LaunchedEffect") - activeWorld?.mapRegionManager?.getRegionForTilesRendering(regionTile) { + activeWorld?.mapRegionManager?.getRegionForTilesRendering(centerTile) { println("MapTileView LaunchedEffect getRegionForTilesRendering, tile: ${this@getRegionForTilesRendering}") val tile = this@getRegionForTilesRendering image = tile.renderWhyImageNow().imageBitmap @@ -56,14 +63,14 @@ fun MapTileView(regionTile: LocalTileRegion) { // Text("Tile $tile") // val t: WhyTiledImage? = tile?.renderWhyImageNow() // val image = t?.imageBitmap - val dpSize = with(LocalDensity.current) { -// DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) - DpSize(image?.width?.toDp() ?: 1.dp, image?.height?.toDp() ?: 1.dp) - } +// val dpSize = with(LocalDensity.current) { +//// DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) +// DpSize(image?.width?.toDp() ?: 1.dp, image?.height?.toDp() ?: 1.dp) +// } // WrappedCanvas(modifier = Modifier.size(dpSize)) { size -> - Canvas(modifier = Modifier.size(dpSize).clipToBounds().pointerInput(Unit) { + Canvas(modifier = Modifier.size(DpSize(400.dp, 400.dp)).clipToBounds().pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offsetX += dragAmount.x / scale @@ -81,7 +88,7 @@ fun MapTileView(regionTile: LocalTileRegion) { image?.let { im -> scale(scale) { // drawImage(im, topLeft = Offset(offsetX, offsetY)) - drawImage(im, dstOffset = IntOffset(offsetX.toInt(), offsetY.toInt()), filterQuality = FilterQuality.None) + drawImage(im, dstOffset = IntOffset(drawOffset.x + offsetX.toInt(), drawOffset.z + offsetY.toInt()), filterQuality = FilterQuality.None) } } // drawIntoCanvas { canvas -> From 8e30bca9fec486fbe06368c1bed337ca110d84c1 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 02:37:37 +0200 Subject: [PATCH 09/23] Draw all tiles, simple cache --- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 2be45f1..5f64032 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds @@ -28,13 +29,15 @@ import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.utils.LocalTileBlock import dev.wefhy.whymap.utils.LocalTileRegion import dev.wefhy.whymap.utils.TileZoom +import dev.wefhy.whymap.utils.WhyDispatchers +import kotlinx.coroutines.launch @OptIn(ExperimentalComposeUiApi::class) @Composable fun MapTileView(regionTile: LocalTileRegion) { - val nRequiredTiles = 3 - + val tileRadius = 1 + val nTiles = 3 var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } var scale by remember { mutableStateOf(1f) } @@ -43,19 +46,26 @@ fun MapTileView(regionTile: LocalTileRegion) { } val block = regionTile.getCenter() - LocalTileBlock(offsetX.toInt(), offsetY.toInt(), TileZoom.BlockZoom) val centerTile = block.parent(TileZoom.RegionZoom) - val drawOffset = centerTile.getCenter() - regionTile.getCenter() - -// var tile: MapArea? by remember { mutableStateOf(null) } - var image: ImageBitmap? by remember { mutableStateOf(null) } - println("MapTileView recompose, image: $image, regionTile: $centerTile") + val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius, TileZoom.RegionZoom) + val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius, TileZoom.RegionZoom) + val dontDispose = remember { mutableSetOf() } + val images: SnapshotStateList = remember { mutableStateListOf(null, null, null, null, null, null, null, null, null) } LaunchedEffect(centerTile) { - image = null - println("MapTileView LaunchedEffect") - activeWorld?.mapRegionManager?.getRegionForTilesRendering(centerTile) { - println("MapTileView LaunchedEffect getRegionForTilesRendering, tile: ${this@getRegionForTilesRendering}") - val tile = this@getRegionForTilesRendering - image = tile.renderWhyImageNow().imageBitmap + for (x in minTile.x..maxTile.x) { + for (z in minTile.z..maxTile.z) { + val tile = LocalTileRegion(x, z, TileZoom.RegionZoom) + val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) + if (tile in dontDispose) continue + images[index] = null + launch(WhyDispatchers.Render) { + activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { + images[index] = renderWhyImageNow().imageBitmap + dontDispose.add(tile) + } + } + } } + dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } } @@ -81,14 +91,21 @@ fun MapTileView(regionTile: LocalTileRegion) { scale *= 1 + scrollDelta.y / 10 } ) { - println("MapTileView Canvas recompose, image: $image") +// println("MapTileView Canvas recompose, image: $image") // drawRect(Rect(Offset(0f, 0f), Size(size.width, size.height)), Paint().apply { color = Color.Black }) // t?.drawTiledImage() drawRect(Color.Black, Offset(0f, 0f), Size(size.width, size.height)) - image?.let { im -> - scale(scale) { + for (y in minTile.z .. maxTile.z) { + for (x in minTile.x .. maxTile.x) { + val index = y.mod(nTiles) * nTiles + x.mod(nTiles) + val image = images[index] + val drawOffset = LocalTileRegion(x, y, TileZoom.RegionZoom).getCenter() - regionTile.getCenter() + image?.let { im -> + scale(scale) { // drawImage(im, topLeft = Offset(offsetX, offsetY)) - drawImage(im, dstOffset = IntOffset(drawOffset.x + offsetX.toInt(), drawOffset.z + offsetY.toInt()), filterQuality = FilterQuality.None) + drawImage(im, dstOffset = IntOffset(drawOffset.x + offsetX.toInt() + 350, drawOffset.z + offsetY.toInt() + 350), filterQuality = FilterQuality.None) + } + } } } // drawIntoCanvas { canvas -> From 87a19402e4f5945d86709b374b25739b5c1651e3 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 17:57:42 +0200 Subject: [PATCH 10/23] Improve render caching and job canceling --- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 41 +++++++++++-------- .../java/dev/wefhy/whymap/utils/MapTile.kt | 14 ++++--- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 5f64032..42d92a7 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -30,7 +30,8 @@ import dev.wefhy.whymap.utils.LocalTileBlock import dev.wefhy.whymap.utils.LocalTileRegion import dev.wefhy.whymap.utils.TileZoom import dev.wefhy.whymap.utils.WhyDispatchers -import kotlinx.coroutines.launch +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext @OptIn(ExperimentalComposeUiApi::class) @@ -44,22 +45,27 @@ fun MapTileView(regionTile: LocalTileRegion) { val paint = Paint().apply { filterQuality = FilterQuality.None } - val block = regionTile.getCenter() - LocalTileBlock(offsetX.toInt(), offsetY.toInt(), TileZoom.BlockZoom) + val block = regionTile.getCenter() - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) val centerTile = block.parent(TileZoom.RegionZoom) - val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius, TileZoom.RegionZoom) - val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius, TileZoom.RegionZoom) + val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius) + val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius) val dontDispose = remember { mutableSetOf() } val images: SnapshotStateList = remember { mutableStateListOf(null, null, null, null, null, null, null, null, null) } - LaunchedEffect(centerTile) { - for (x in minTile.x..maxTile.x) { - for (z in minTile.z..maxTile.z) { - val tile = LocalTileRegion(x, z, TileZoom.RegionZoom) - val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) - if (tile in dontDispose) continue + + + for (x in minTile.x..maxTile.x) { + for (z in minTile.z..maxTile.z) { + val tile = LocalTileRegion(x, z) + val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) + LaunchedEffect(tile) { + if (tile in dontDispose) return@LaunchedEffect images[index] = null - launch(WhyDispatchers.Render) { + withContext(WhyDispatchers.Render) { activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { - images[index] = renderWhyImageNow().imageBitmap + if (!isActive) return@getRegionForTilesRendering + val image = renderWhyImageNow().imageBitmap + if (!isActive) return@getRegionForTilesRendering + images[index] = image dontDispose.add(tile) } } @@ -67,8 +73,6 @@ fun MapTileView(regionTile: LocalTileRegion) { } dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } } - - Column { // Text("Tile $tile") // val t: WhyTiledImage? = tile?.renderWhyImageNow() @@ -102,8 +106,13 @@ fun MapTileView(regionTile: LocalTileRegion) { val drawOffset = LocalTileRegion(x, y, TileZoom.RegionZoom).getCenter() - regionTile.getCenter() image?.let { im -> scale(scale) { -// drawImage(im, topLeft = Offset(offsetX, offsetY)) - drawImage(im, dstOffset = IntOffset(drawOffset.x + offsetX.toInt() + 350, drawOffset.z + offsetY.toInt() + 350), filterQuality = FilterQuality.None) + val drawX = drawOffset.x + offsetX + 350 + val drawY = drawOffset.z + offsetY + 350 + if (scale > 1) { + drawImage(im, dstOffset = IntOffset(drawX.toInt(), drawY.toInt()), filterQuality = FilterQuality.None) + } else { + drawImage(im, topLeft = Offset(drawX, drawY)) + } } } } diff --git a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt index 3cb0338..76afb4d 100644 --- a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt @@ -11,8 +11,8 @@ open class MapTile(val x: Int, val z: Int, val zoom: Z) where Z : TileZoom { fun toLocalTile(): LocalTile { return LocalTile( - x - zoom.shift, - z - zoom.shift, + x - zoom.offset, + z - zoom.offset, zoom ) } @@ -51,16 +51,20 @@ open class MapTile(val x: Int, val z: Int, val zoom: Z) where Z : TileZoom { } typealias LocalTileBlock = LocalTile +fun LocalTileBlock(x: Int, z: Int) = LocalTile(x, z, BlockZoom) typealias LocalTileChunk = LocalTile +fun LocalTileChunk(x: Int, z: Int) = LocalTile(x, z, ChunkZoom) typealias LocalTileRegion = LocalTile +fun LocalTileRegion(x: Int, z: Int) = LocalTile(x, z, RegionZoom) typealias LocalTileThumbnail = LocalTile +fun LocalTileThumbnail(x: Int, z: Int) = LocalTile(x, z, ThumbnailZoom) open class LocalTile(val x: Int, val z: Int, val zoom: Z) { fun toMapTile(): MapTile { return MapTile( - x + zoom.shift, - z + zoom.shift, + x + zoom.offset, + z + zoom.offset, zoom ) } @@ -189,7 +193,7 @@ sealed class TileZoom(val zoom: Int) { object RegionZoom : TileZoom(WhyMapConfig.regionZoom) object ThumbnailZoom : TileZoom(WhyMapConfig.thumbnailZoom) - val shift = 1 shl (zoom - 1) + val offset = 1 shl (zoom - 1) } fun File.resolve(tile: MapTile) = this From b54ccd76fb7ae715637707da153df8b5782ed202 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 18:21:24 +0200 Subject: [PATCH 11/23] Better zoom offset --- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 6 ++-- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 33 +++++++++++-------- .../java/dev/wefhy/whymap/utils/MapTile.kt | 2 ++ .../dev/wefhy/whymap/whygraphics/WhyTile.kt | 8 ++--- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 188fa5b..96f1b77 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -24,9 +24,9 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod import dev.wefhy.whymap.compose.ComposeView +import dev.wefhy.whymap.utils.Accessors.clientInstance import dev.wefhy.whymap.utils.Accessors.clientWindow -import dev.wefhy.whymap.utils.MapTile -import dev.wefhy.whymap.utils.TileZoom +import dev.wefhy.whymap.utils.LocalTileBlock import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text @@ -111,7 +111,7 @@ class ConfigScreen : Screen(Text.of("Config")) { ) { Column { Text("Map") - MapTileView(MapTile(65532, 65543, TileZoom.RegionZoom).toLocalTile()) + MapTileView(LocalTileBlock(clientInstance.player!!.pos)) } // MapTileView(LocalTileThumbnail(16383, 16384, TileZoom.ThumbnailZoom)) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 42d92a7..53ea112 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerInput @@ -36,7 +37,7 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalComposeUiApi::class) @Composable -fun MapTileView(regionTile: LocalTileRegion) { +fun MapTileView(startPosition: LocalTileBlock) { val tileRadius = 1 val nTiles = 3 var offsetX by remember { mutableStateOf(0f) } @@ -45,7 +46,7 @@ fun MapTileView(regionTile: LocalTileRegion) { val paint = Paint().apply { filterQuality = FilterQuality.None } - val block = regionTile.getCenter() - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) + val block = startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) val centerTile = block.parent(TileZoom.RegionZoom) val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius) val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius) @@ -56,15 +57,17 @@ fun MapTileView(regionTile: LocalTileRegion) { for (x in minTile.x..maxTile.x) { for (z in minTile.z..maxTile.z) { val tile = LocalTileRegion(x, z) + if (tile in dontDispose) continue val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) + images[index] = null LaunchedEffect(tile) { - if (tile in dontDispose) return@LaunchedEffect - images[index] = null + println("MapTileView LaunchedEffect, tile: $tile, index: $index") +// if (tile in dontDispose) return@LaunchedEffect withContext(WhyDispatchers.Render) { activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { - if (!isActive) return@getRegionForTilesRendering + if (!isActive) return@getRegionForTilesRendering Unit.also { println("Cancel early 1")} val image = renderWhyImageNow().imageBitmap - if (!isActive) return@getRegionForTilesRendering + if (!isActive) return@getRegionForTilesRendering Unit.also { println("Cancel early 2")} images[index] = image dontDispose.add(tile) } @@ -103,15 +106,17 @@ fun MapTileView(regionTile: LocalTileRegion) { for (x in minTile.x .. maxTile.x) { val index = y.mod(nTiles) * nTiles + x.mod(nTiles) val image = images[index] - val drawOffset = LocalTileRegion(x, y, TileZoom.RegionZoom).getCenter() - regionTile.getCenter() + val drawOffset = LocalTileRegion(x, y, TileZoom.RegionZoom).getCenter() - startPosition image?.let { im -> - scale(scale) { - val drawX = drawOffset.x + offsetX + 350 - val drawY = drawOffset.z + offsetY + 350 - if (scale > 1) { - drawImage(im, dstOffset = IntOffset(drawX.toInt(), drawY.toInt()), filterQuality = FilterQuality.None) - } else { - drawImage(im, topLeft = Offset(drawX, drawY)) + translate(offsetX * scale, offsetY * scale) { + scale(scale) { + val drawX = drawOffset.x + 350 + val drawY = drawOffset.z + 350 + if (scale > 1) { + drawImage(im, dstOffset = IntOffset(drawX.toInt(), drawY.toInt()), filterQuality = FilterQuality.None) + } else { + drawImage(im, topLeft = Offset(drawX.toFloat(), drawY.toFloat())) + } } } } diff --git a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt index 76afb4d..18e5b0c 100644 --- a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt @@ -5,6 +5,7 @@ package dev.wefhy.whymap.utils import dev.wefhy.whymap.config.WhyMapConfig import dev.wefhy.whymap.utils.TileZoom.* import net.minecraft.util.math.ChunkPos +import net.minecraft.util.math.Vec3d import java.io.File open class MapTile(val x: Int, val z: Int, val zoom: Z) where Z : TileZoom { @@ -52,6 +53,7 @@ open class MapTile(val x: Int, val z: Int, val zoom: Z) where Z : TileZoom { typealias LocalTileBlock = LocalTile fun LocalTileBlock(x: Int, z: Int) = LocalTile(x, z, BlockZoom) +fun LocalTileBlock(pos: Vec3d) = LocalTileBlock(pos.x.toInt(), pos.z.toInt()) typealias LocalTileChunk = LocalTile fun LocalTileChunk(x: Int, z: Int) = LocalTile(x, z, ChunkZoom) typealias LocalTileRegion = LocalTile diff --git a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt index 09add82..3ff9bc9 100644 --- a/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt +++ b/src/main/java/dev/wefhy/whymap/whygraphics/WhyTile.kt @@ -119,10 +119,10 @@ open class WhyTile(val data: Array = Array(arraySize) { WhyColor.Trans bytes = bytes, rowBytes = width * 4 ) - return image.toComposeImageBitmap().also { - println("Converted to ImageBitmap: " + - "${it.colorSpace}, ${it.width}, ${it.height}, ${it.config}") - } + return image.toComposeImageBitmap()//.also { +// println("Converted to ImageBitmap: " + +// "${it.colorSpace}, ${it.width}, ${it.height}, ${it.config}") +// } } fun writeInto(raster: WritableRaster, xOffset: Int, yOffset: Int) { From 2d6093cf3ca588747906d47809c89765a4f47dcc Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 19:30:35 +0200 Subject: [PATCH 12/23] refactor MapTileView pt1 --- .../wefhy/whymap/compose/ui/ComposeUtils.kt | 12 +++++++ .../dev/wefhy/whymap/compose/ui/MapTile.kt | 32 ++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt new file mode 100644 index 0000000..2c14ba7 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt @@ -0,0 +1,12 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.toOffset +import dev.wefhy.whymap.utils.LocalTileBlock + +object ComposeUtils { + fun LocalTileBlock.toOffset(): Offset = androidx.compose.ui.unit.IntOffset(x, z).toOffset() + fun Offset.toLocalTileBlock(): LocalTileBlock = LocalTileBlock(x.toInt(), y.toInt()) +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 53ea112..efec84e 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -27,6 +27,8 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld +import dev.wefhy.whymap.compose.ui.ComposeUtils.toLocalTileBlock +import dev.wefhy.whymap.compose.ui.ComposeUtils.toOffset import dev.wefhy.whymap.utils.LocalTileBlock import dev.wefhy.whymap.utils.LocalTileRegion import dev.wefhy.whymap.utils.TileZoom @@ -38,15 +40,22 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalComposeUiApi::class) @Composable fun MapTileView(startPosition: LocalTileBlock) { + val scope = rememberCoroutineScope() val tileRadius = 1 val nTiles = 3 - var offsetX by remember { mutableStateOf(0f) } - var offsetY by remember { mutableStateOf(0f) } +// var offsetX by remember { mutableStateOf(0f) } +// var offsetY by remember { mutableStateOf(0f) } var scale by remember { mutableStateOf(1f) } + var center by remember { mutableStateOf(startPosition.toOffset()) } val paint = Paint().apply { filterQuality = FilterQuality.None } - val block = startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) + val block by remember { derivedStateOf { center.toLocalTileBlock() }} //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) +// val offsetX = center.x - startPosition.x +// val offsetY = center.y - startPosition.z +// rememberSaveable() + val offsetX = startPosition.x - center.x + val offsetY = startPosition.z - center.y val centerTile = block.parent(TileZoom.RegionZoom) val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius) val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius) @@ -59,19 +68,19 @@ fun MapTileView(startPosition: LocalTileBlock) { val tile = LocalTileRegion(x, z) if (tile in dontDispose) continue val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) - images[index] = null LaunchedEffect(tile) { + images[index] = null println("MapTileView LaunchedEffect, tile: $tile, index: $index") // if (tile in dontDispose) return@LaunchedEffect - withContext(WhyDispatchers.Render) { + val image = withContext(WhyDispatchers.Render) { activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { - if (!isActive) return@getRegionForTilesRendering Unit.also { println("Cancel early 1")} - val image = renderWhyImageNow().imageBitmap - if (!isActive) return@getRegionForTilesRendering Unit.also { println("Cancel early 2")} - images[index] = image - dontDispose.add(tile) + if (!isActive) return@getRegionForTilesRendering null.also { println("Cancel early 1")} + renderWhyImageNow().imageBitmap } } + if (!isActive) return@LaunchedEffect Unit.also { println("Cancel early 2")} + images[index] = image + dontDispose.add(tile) } } dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } @@ -90,8 +99,7 @@ fun MapTileView(startPosition: LocalTileBlock) { Canvas(modifier = Modifier.size(DpSize(400.dp, 400.dp)).clipToBounds().pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() - offsetX += dragAmount.x / scale - offsetY += dragAmount.y / scale + center -= dragAmount / scale } }.onPointerEvent(PointerEventType.Scroll) { val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } From 3b52f3d4753c66fdad64cb611cf8d43713636f69 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 20:11:00 +0200 Subject: [PATCH 13/23] refactor MapTileView pt2 --- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 2 + .../dev/wefhy/whymap/compose/ui/MapTile.kt | 56 ++++++------------- 2 files changed, 20 insertions(+), 38 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 96f1b77..a588933 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandIn import androidx.compose.animation.shrinkOut import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -73,6 +74,7 @@ class ConfigScreen : Screen(Text.of("Config")) { var showList by remember { mutableStateOf(true) } var showMap by remember { mutableStateOf(false) } Card( + border = BorderStroke(1.dp, Color(0.05f, 0.1f, 0.2f)), elevation = 20.dp, modifier = Modifier.padding(200.dp, 0.dp, 0.dp, 0.dp).padding(8.dp)/*.onPointerEvent(PointerEventType.Move) { val position = it.changes.first().position color = Color(position.x.toInt() % 256, position.y.toInt() % 256, 0) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index efec84e..a637e0c 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -43,19 +43,12 @@ fun MapTileView(startPosition: LocalTileBlock) { val scope = rememberCoroutineScope() val tileRadius = 1 val nTiles = 3 -// var offsetX by remember { mutableStateOf(0f) } -// var offsetY by remember { mutableStateOf(0f) } var scale by remember { mutableStateOf(1f) } var center by remember { mutableStateOf(startPosition.toOffset()) } val paint = Paint().apply { filterQuality = FilterQuality.None } val block by remember { derivedStateOf { center.toLocalTileBlock() }} //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) -// val offsetX = center.x - startPosition.x -// val offsetY = center.y - startPosition.z -// rememberSaveable() - val offsetX = startPosition.x - center.x - val offsetY = startPosition.z - center.y val centerTile = block.parent(TileZoom.RegionZoom) val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius) val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius) @@ -71,7 +64,6 @@ fun MapTileView(startPosition: LocalTileBlock) { LaunchedEffect(tile) { images[index] = null println("MapTileView LaunchedEffect, tile: $tile, index: $index") -// if (tile in dontDispose) return@LaunchedEffect val image = withContext(WhyDispatchers.Render) { activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { if (!isActive) return@getRegionForTilesRendering null.also { println("Cancel early 1")} @@ -86,57 +78,45 @@ fun MapTileView(startPosition: LocalTileBlock) { dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } } Column { -// Text("Tile $tile") -// val t: WhyTiledImage? = tile?.renderWhyImageNow() -// val image = t?.imageBitmap // val dpSize = with(LocalDensity.current) { //// DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) // DpSize(image?.width?.toDp() ?: 1.dp, image?.height?.toDp() ?: 1.dp) // } - -// WrappedCanvas(modifier = Modifier.size(dpSize)) { size -> - Canvas(modifier = Modifier.size(DpSize(400.dp, 400.dp)).clipToBounds().pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() - center -= dragAmount / scale + Canvas(modifier = Modifier + .size(DpSize(400.dp, 400.dp)) + .clipToBounds() + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + center -= dragAmount / scale + } + } + .onPointerEvent(PointerEventType.Scroll) { + val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } + scale *= 1 + scrollDelta.y / 10 } - }.onPointerEvent(PointerEventType.Scroll) { - val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } - scale *= 1 + scrollDelta.y / 10 - } ) { -// println("MapTileView Canvas recompose, image: $image") -// drawRect(Rect(Offset(0f, 0f), Size(size.width, size.height)), Paint().apply { color = Color.Black }) -// t?.drawTiledImage() - drawRect(Color.Black, Offset(0f, 0f), Size(size.width, size.height)) +// drawRect(Color.Black, Offset(0f, 0f), Size(size.width, size.height)) for (y in minTile.z .. maxTile.z) { for (x in minTile.x .. maxTile.x) { val index = y.mod(nTiles) * nTiles + x.mod(nTiles) val image = images[index] - val drawOffset = LocalTileRegion(x, y, TileZoom.RegionZoom).getCenter() - startPosition + val drawOffset = LocalTileRegion(x, y).getCenter() image?.let { im -> - translate(offsetX * scale, offsetY * scale) { - scale(scale) { - val drawX = drawOffset.x + 350 - val drawY = drawOffset.z + 350 + scale(scale) { + translate( - center.x, - center.y) { if (scale > 1) { - drawImage(im, dstOffset = IntOffset(drawX.toInt(), drawY.toInt()), filterQuality = FilterQuality.None) + drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) } else { - drawImage(im, topLeft = Offset(drawX.toFloat(), drawY.toFloat())) + drawImage(im, topLeft = drawOffset.toOffset()) } } } } } } -// drawIntoCanvas { canvas -> -// with(canvas) { -// t?.drawTiledImage() -// } -// } } - } } From cf013e6f4c13b772e5d59805263b21d6386a6826 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 22:47:02 +0200 Subject: [PATCH 14/23] refactor MapTileView pt3 --- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 162 ++++++++++-------- .../java/dev/wefhy/whymap/compose/ui/Icons.kt | 136 +++++++++++++++ .../dev/wefhy/whymap/compose/ui/MapTile.kt | 43 +++-- .../wefhy/whymap/compose/ui/MapViewModel.kt | 11 ++ .../wefhy/whymap/compose/ui/WaypointsView.kt | 4 +- src/main/java/dev/wefhy/whymap/utils/Utils.kt | 4 +- 6 files changed, 264 insertions(+), 96 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/Icons.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index a588933..4feb01d 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -2,20 +2,15 @@ package dev.wefhy.whymap.compose.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandIn -import androidx.compose.animation.shrinkOut +import androidx.compose.animation.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.Card -import androidx.compose.material.Switch -import androidx.compose.material.Text +import androidx.compose.material.* +import androidx.compose.material.icons.Icons import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -32,27 +27,30 @@ import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text import java.util.* -import kotlin.random.Random class ConfigScreen : Screen(Text.of("Config")) { - companion object { - private var initializationCount = 0 - } - - var i = 0 - val random = Random(0) + private val vm = MapViewModel() private val composeView = ComposeView( width = clientWindow.width, height = clientWindow.height, density = Density(3f) ) { - UI() + var visible by remember { mutableStateOf(false) } + MaterialTheme(colors = if(vm.isDarkTheme) darkColors() else lightColors()) { //todo change theme according to minecraft day/night or real life + LaunchedEffect(Unit) { + visible = true + } + AnimatedVisibility(visible, enter = scaleIn() + fadeIn()) { + UI(vm) + } +// Scaffold { +// } + } } - init { - println("ConfigScreen init ${++initializationCount}") +// init { // RenderThreadScope.launch { // while (true) { //// composeView.passLMBClick(148.8671875f, 43.724609375f) @@ -63,23 +61,64 @@ class ConfigScreen : Screen(Text.of("Config")) { // delay(random.nextLong(50, 550)) // } // } +// } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { +// super.render(context, mouseX, mouseY, delta) + composeView.render(context, delta) + } + + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { +// println("Mouse clicked at $mouseX, $mouseY") + composeView.passLMBClick(mouseX.toFloat(), mouseY.toFloat()) + return super.mouseClicked(mouseX, mouseY, button) + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + composeView.passMouseMove(mouseX.toFloat(), mouseY.toFloat()) + super.mouseMoved(mouseX, mouseY) } - @OptIn(ExperimentalComposeUiApi::class) - @Preview - @Composable - fun UI() { - var clicks by remember { mutableStateOf(0) } - var color by remember { mutableStateOf(Color.Green) } - var showList by remember { mutableStateOf(true) } - var showMap by remember { mutableStateOf(false) } - Card( - border = BorderStroke(1.dp, Color(0.05f, 0.1f, 0.2f)), - elevation = 20.dp, modifier = Modifier.padding(200.dp, 0.dp, 0.dp, 0.dp).padding(8.dp)/*.onPointerEvent(PointerEventType.Move) { + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { +// println("Mouse released at $mouseX, $mouseY") + composeView.passLMBRelease(mouseX.toFloat(), mouseY.toFloat()) + return super.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { +// println("Mouse scrolled at $mouseX, $mouseY, $horizontalAmount, $verticalAmount") + composeView.passScroll(mouseX.toFloat(), mouseY.toFloat(), horizontalAmount.toFloat(), verticalAmount.toFloat()) + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + } + + override fun close() { + composeView.close() + super.close() + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun UI(vm: MapViewModel) { + var i by remember { mutableStateOf(0) } + var clicks by remember { mutableStateOf(0) } + var color by remember { mutableStateOf(Color.Green) } + var showList by remember { mutableStateOf(true) } + var showMap by remember { mutableStateOf(false) } + Card( + border = BorderStroke(1.dp, Color(0.05f, 0.1f, 0.2f)), + elevation = 20.dp, modifier = Modifier.padding(200.dp, 0.dp, 0.dp, 0.dp).padding(8.dp)/*.onPointerEvent(PointerEventType.Move) { val position = it.changes.first().position color = Color(position.x.toInt() % 256, position.y.toInt() % 256, 0) }*/ - ) { + ) { + Box { Row(Modifier.padding(8.dp)) { println("Recomposition ${i++}") Column { @@ -125,7 +164,7 @@ class ConfigScreen : Screen(Text.of("Config")) { exit = shrinkOut() ) { val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() - val entries = waypoints.mapIndexed() { i, it -> + val entries = waypoints.mapIndexed { i, it -> WaypointEntry( name = it.name, distance = 0.0f, waypointId = i, date = Date(), waypointStatus = WaypointEntry.Status.NEW, waypointType = WaypointEntry.Type.SIGHTSEEING ) @@ -133,9 +172,9 @@ class ConfigScreen : Screen(Text.of("Config")) { WaypointsView(entries) { println("Refresh!") } - val rememberScrollState = rememberScrollState() +// val rememberScrollState = rememberScrollState() // Column(Modifier.scrollable(rememberScrollState, orientation = Orientation.Vertical)) { - Column(Modifier.verticalScroll(rememberScrollState)) { +// Column(Modifier.verticalScroll(rememberScrollState)) { // for (it in 0..20) { // val hovered = remember { mutableStateOf(false) } // Text("Item $it", Modifier.background(if (hovered.value) Color.Gray else Color.Transparent).padding(8.dp).onPointerEvent( @@ -150,48 +189,25 @@ class ConfigScreen : Screen(Text.of("Config")) { // WaypointsView(entries) { // println("Refresh!") // } - } +// } } } + FloatingActionButton(onClick = { vm.isDarkTheme = !vm.isDarkTheme }, Modifier.align(Alignment.TopEnd).padding(8.dp)) { + val im = if (vm.isDarkTheme) Icons.TwoTone.ModeNight else Icons.TwoTone.WbSunny + Icon(im, contentDescription = "Theme") + } } } - - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { -// super.render(context, mouseX, mouseY, delta) - composeView.render(context, delta) - } - - - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { -// println("Mouse clicked at $mouseX, $mouseY") - composeView.passLMBClick(mouseX.toFloat(), mouseY.toFloat()) - return super.mouseClicked(mouseX, mouseY, button) - } - - override fun mouseMoved(mouseX: Double, mouseY: Double) { - composeView.passMouseMove(mouseX.toFloat(), mouseY.toFloat()) - super.mouseMoved(mouseX, mouseY) - } - - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { -// println("Mouse released at $mouseX, $mouseY") - composeView.passLMBRelease(mouseX.toFloat(), mouseY.toFloat()) - return super.mouseReleased(mouseX, mouseY, button) - } - - override fun mouseScrolled( - mouseX: Double, - mouseY: Double, - horizontalAmount: Double, - verticalAmount: Double - ): Boolean { -// println("Mouse scrolled at $mouseX, $mouseY, $horizontalAmount, $verticalAmount") - composeView.passScroll(mouseX.toFloat(), mouseY.toFloat(), horizontalAmount.toFloat(), verticalAmount.toFloat()) - return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) - } - - override fun close() { - composeView.close() - super.close() +} + +@Preview +@Composable +private fun preview() { + val vm = MapViewModel() + vm.isDarkTheme = true + MaterialTheme(colors = if(vm.isDarkTheme) darkColors() else lightColors()) { + Scaffold { + UI(vm) + } } } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/Icons.kt b/src/main/java/dev/wefhy/whymap/compose/ui/Icons.kt new file mode 100644 index 0000000..cd76dc9 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/Icons.kt @@ -0,0 +1,136 @@ +package dev.wefhy.whymap.compose.ui + +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.materialIcon +import androidx.compose.material.icons.materialPath +import androidx.compose.ui.graphics.vector.ImageVector + +val Icons.TwoTone.ModeNight: ImageVector + get() { + if (_modeNight != null) { + return _modeNight!! + } + _modeNight = materialIcon(name = "TwoTone.ModeNight") { + materialPath(fillAlpha = 0.3f, strokeAlpha = 0.3f) { + moveTo(9.5f, 4.0f) + curveTo(9.16f, 4.0f, 8.82f, 4.02f, 8.49f, 4.07f) + curveTo(10.4f, 6.23f, 11.5f, 9.05f, 11.5f, 12.0f) + reflectiveCurveToRelative(-1.1f, 5.77f, -3.01f, 7.93f) + curveTo(8.82f, 19.98f, 9.16f, 20.0f, 9.5f, 20.0f) + curveToRelative(4.41f, 0.0f, 8.0f, -3.59f, 8.0f, -8.0f) + reflectiveCurveTo(13.91f, 4.0f, 9.5f, 4.0f) + close() + } + materialPath { + moveTo(9.5f, 2.0f) + curveToRelative(-1.82f, 0.0f, -3.53f, 0.5f, -5.0f, 1.35f) + curveToRelative(2.99f, 1.73f, 5.0f, 4.95f, 5.0f, 8.65f) + reflectiveCurveToRelative(-2.01f, 6.92f, -5.0f, 8.65f) + curveTo(5.97f, 21.5f, 7.68f, 22.0f, 9.5f, 22.0f) + curveToRelative(5.52f, 0.0f, 10.0f, -4.48f, 10.0f, -10.0f) + reflectiveCurveTo(15.02f, 2.0f, 9.5f, 2.0f) + close() + moveTo(9.5f, 20.0f) + curveToRelative(-0.34f, 0.0f, -0.68f, -0.02f, -1.01f, -0.07f) + curveToRelative(1.91f, -2.16f, 3.01f, -4.98f, 3.01f, -7.93f) + reflectiveCurveToRelative(-1.1f, -5.77f, -3.01f, -7.93f) + curveTo(8.82f, 4.02f, 9.16f, 4.0f, 9.5f, 4.0f) + curveToRelative(4.41f, 0.0f, 8.0f, 3.59f, 8.0f, 8.0f) + reflectiveCurveTo(13.91f, 20.0f, 9.5f, 20.0f) + close() + } + } + return _modeNight!! + } + +private var _modeNight: ImageVector? = null + +val Icons.TwoTone.WbSunny: ImageVector + get() { + if (_wbSunny != null) { + return _wbSunny!! + } + _wbSunny = materialIcon(name = "TwoTone.WbSunny") { + materialPath(fillAlpha = 0.3f, strokeAlpha = 0.3f) { + moveTo(12.0f, 7.5f) + curveToRelative(-2.21f, 0.0f, -4.0f, 1.79f, -4.0f, 4.0f) + reflectiveCurveToRelative(1.79f, 4.0f, 4.0f, 4.0f) + reflectiveCurveToRelative(4.0f, -1.79f, 4.0f, -4.0f) + reflectiveCurveToRelative(-1.79f, -4.0f, -4.0f, -4.0f) + close() + } + materialPath { + moveTo(5.34f, 6.25f) + lineToRelative(1.42f, -1.41f) + lineToRelative(-1.8f, -1.79f) + lineToRelative(-1.41f, 1.41f) + close() + moveTo(1.0f, 10.5f) + horizontalLineToRelative(3.0f) + verticalLineToRelative(2.0f) + lineTo(1.0f, 12.5f) + close() + moveTo(11.0f, 0.55f) + horizontalLineToRelative(2.0f) + lineTo(13.0f, 3.5f) + horizontalLineToRelative(-2.0f) + close() + moveTo(18.66f, 6.255f) + lineToRelative(-1.41f, -1.407f) + lineToRelative(1.79f, -1.79f) + lineToRelative(1.406f, 1.41f) + close() + moveTo(17.24f, 18.16f) + lineToRelative(1.79f, 1.8f) + lineToRelative(1.41f, -1.41f) + lineToRelative(-1.8f, -1.79f) + close() + moveTo(20.0f, 10.5f) + horizontalLineToRelative(3.0f) + verticalLineToRelative(2.0f) + horizontalLineToRelative(-3.0f) + close() + moveTo(12.0f, 5.5f) + curveToRelative(-3.31f, 0.0f, -6.0f, 2.69f, -6.0f, 6.0f) + reflectiveCurveToRelative(2.69f, 6.0f, 6.0f, 6.0f) + reflectiveCurveToRelative(6.0f, -2.69f, 6.0f, -6.0f) + reflectiveCurveToRelative(-2.69f, -6.0f, -6.0f, -6.0f) + close() + moveTo(12.0f, 15.5f) + curveToRelative(-2.21f, 0.0f, -4.0f, -1.79f, -4.0f, -4.0f) + reflectiveCurveToRelative(1.79f, -4.0f, 4.0f, -4.0f) + reflectiveCurveToRelative(4.0f, 1.79f, 4.0f, 4.0f) + reflectiveCurveToRelative(-1.79f, 4.0f, -4.0f, 4.0f) + close() + moveTo(11.0f, 19.5f) + horizontalLineToRelative(2.0f) + verticalLineToRelative(2.95f) + horizontalLineToRelative(-2.0f) + close() + moveTo(3.55f, 18.54f) + lineToRelative(1.41f, 1.41f) + lineToRelative(1.79f, -1.8f) + lineToRelative(-1.41f, -1.41f) + close() + } + } + return _wbSunny!! + } + +private var _wbSunny: ImageVector? = null diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index a637e0c..6d3213d 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -3,13 +3,13 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size +import androidx.compose.material.Card import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.compose.ui.ComposeUtils.toLocalTileBlock import dev.wefhy.whymap.compose.ui.ComposeUtils.toOffset +import dev.wefhy.whymap.config.WhyMapConfig.storageTileBlocks import dev.wefhy.whymap.utils.LocalTileBlock import dev.wefhy.whymap.utils.LocalTileRegion import dev.wefhy.whymap.utils.TileZoom @@ -45,24 +46,22 @@ fun MapTileView(startPosition: LocalTileBlock) { val nTiles = 3 var scale by remember { mutableStateOf(1f) } var center by remember { mutableStateOf(startPosition.toOffset()) } - val paint = Paint().apply { - filterQuality = FilterQuality.None - } val block by remember { derivedStateOf { center.toLocalTileBlock() }} //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) val centerTile = block.parent(TileZoom.RegionZoom) val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius) val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius) val dontDispose = remember { mutableSetOf() } - val images: SnapshotStateList = remember { mutableStateListOf(null, null, null, null, null, null, null, null, null) } - + val images = remember { mutableStateMapOf() } + dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } for (x in minTile.x..maxTile.x) { for (z in minTile.z..maxTile.z) { val tile = LocalTileRegion(x, z) if (tile in dontDispose) continue val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) LaunchedEffect(tile) { - images[index] = null + assert(tile !in images) +// images.remove(tile) //TODO actually just return if already loaded. But this should be handled by dontDispose println("MapTileView LaunchedEffect, tile: $tile, index: $index") val image = withContext(WhyDispatchers.Render) { activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { @@ -71,13 +70,17 @@ fun MapTileView(startPosition: LocalTileBlock) { } } if (!isActive) return@LaunchedEffect Unit.also { println("Cancel early 2")} - images[index] = image + image?.let { + images[tile] = it + } dontDispose.add(tile) } } - dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } } - Column { + + Card( + elevation = 8.dp + ) { // val dpSize = with(LocalDensity.current) { //// DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) // DpSize(image?.width?.toDp() ?: 1.dp, image?.height?.toDp() ?: 1.dp) @@ -85,6 +88,7 @@ fun MapTileView(startPosition: LocalTileBlock) { Canvas(modifier = Modifier .size(DpSize(400.dp, 400.dp)) + .background(Color(0.1f, 0.1f, 0.1f)) .clipToBounds() .pointerInput(Unit) { detectDragGestures { change, dragAmount -> @@ -101,15 +105,18 @@ fun MapTileView(startPosition: LocalTileBlock) { for (y in minTile.z .. maxTile.z) { for (x in minTile.x .. maxTile.x) { val index = y.mod(nTiles) * nTiles + x.mod(nTiles) - val image = images[index] - val drawOffset = LocalTileRegion(x, y).getCenter() + val tile = LocalTileRegion(x, y) + val image = images[tile] + val drawOffset = tile.getCenter() image?.let { im -> scale(scale) { - translate( - center.x, - center.y) { - if (scale > 1) { - drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) - } else { - drawImage(im, topLeft = drawOffset.toOffset()) + translate(storageTileBlocks.toFloat() / 2, storageTileBlocks.toFloat() / 2) { + translate( - center.x, - center.y) { + if (scale > 1) { + drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) + } else { + drawImage(im, topLeft = drawOffset.toOffset()) + } } } } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt new file mode 100644 index 0000000..9475b0f --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt @@ -0,0 +1,11 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class MapViewModel { + var isDarkTheme by mutableStateOf(false) +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index 332cffa..4022352 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -106,12 +106,12 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit) { Box(Modifier.pullRefresh(state).clipToBounds()) { val scrollState = rememberScrollState() Column( - modifier = Modifier.verticalScroll(scrollState), + modifier = Modifier.verticalScroll(scrollState).padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), // contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp) ) { for (waypoint in waypoints) { - Box(Modifier.padding(8.dp)) { + Box { WaypointEntryView(waypoint) } } diff --git a/src/main/java/dev/wefhy/whymap/utils/Utils.kt b/src/main/java/dev/wefhy/whymap/utils/Utils.kt index dba59ba..c7c2236 100644 --- a/src/main/java/dev/wefhy/whymap/utils/Utils.kt +++ b/src/main/java/dev/wefhy/whymap/utils/Utils.kt @@ -210,13 +210,11 @@ fun Raster.fillWithColor(color: Int) { } } -val R = Random(0) - fun WritableRaster.fillWithColor2(color: Int) { println("${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}, ${minX} ${minY} ${width} ${height}") for (y in 0 until height) { for (x in 0 until width) { - setPixel(x, y, intArrayOf(color, R.nextInt(), R.nextInt())) + setPixel(x, y, intArrayOf(color, rand.nextInt(), rand.nextInt())) } } } From 7a471a05c81132f6d37f79d76fcc45a7a88c1f7f Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Mon, 10 Jun 2024 23:33:42 +0200 Subject: [PATCH 15/23] Better center, fix recompositions --- .../java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt | 9 +++++++++ .../java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt | 8 +++----- src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt | 9 ++++----- 3 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt new file mode 100644 index 0000000..ee021d0 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt @@ -0,0 +1,9 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +object ComposeConstants { + const val minScale = 0.001f + const val maxScale = 50f + val scaleRange = minScale..maxScale +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 4feb01d..e69a1b7 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -103,20 +103,18 @@ class ConfigScreen : Screen(Text.of("Config")) { } } +private var i = 0 + @OptIn(ExperimentalComposeUiApi::class) @Composable private fun UI(vm: MapViewModel) { - var i by remember { mutableStateOf(0) } var clicks by remember { mutableStateOf(0) } var color by remember { mutableStateOf(Color.Green) } var showList by remember { mutableStateOf(true) } var showMap by remember { mutableStateOf(false) } Card( border = BorderStroke(1.dp, Color(0.05f, 0.1f, 0.2f)), - elevation = 20.dp, modifier = Modifier.padding(200.dp, 0.dp, 0.dp, 0.dp).padding(8.dp)/*.onPointerEvent(PointerEventType.Move) { - val position = it.changes.first().position - color = Color(position.x.toInt() % 256, position.y.toInt() % 256, 0) - }*/ + elevation = 20.dp, modifier = Modifier/*.padding(200.dp, 0.dp, 0.dp, 0.dp)*/.padding(8.dp) ) { Box { Row(Modifier.padding(8.dp)) { diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 6d3213d..03fdecc 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -27,9 +27,9 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld +import dev.wefhy.whymap.compose.ui.ComposeConstants.scaleRange import dev.wefhy.whymap.compose.ui.ComposeUtils.toLocalTileBlock import dev.wefhy.whymap.compose.ui.ComposeUtils.toOffset -import dev.wefhy.whymap.config.WhyMapConfig.storageTileBlocks import dev.wefhy.whymap.utils.LocalTileBlock import dev.wefhy.whymap.utils.LocalTileRegion import dev.wefhy.whymap.utils.TileZoom @@ -98,19 +98,18 @@ fun MapTileView(startPosition: LocalTileBlock) { } .onPointerEvent(PointerEventType.Scroll) { val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } - scale *= 1 + scrollDelta.y / 10 + scale = (scale * (1 + scrollDelta.y / 10)).coerceIn(scaleRange) } ) { // drawRect(Color.Black, Offset(0f, 0f), Size(size.width, size.height)) for (y in minTile.z .. maxTile.z) { for (x in minTile.x .. maxTile.x) { - val index = y.mod(nTiles) * nTiles + x.mod(nTiles) val tile = LocalTileRegion(x, y) val image = images[tile] - val drawOffset = tile.getCenter() + val drawOffset = tile.getStart() image?.let { im -> scale(scale) { - translate(storageTileBlocks.toFloat() / 2, storageTileBlocks.toFloat() / 2) { + translate(size.width / 2, size.height / 2) { translate( - center.x, - center.y) { if (scale > 1) { drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) From 558d0134ce931ad2a6f9285f602790a611f636ca Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Tue, 11 Jun 2024 22:10:12 +0200 Subject: [PATCH 16/23] Fix lazy layouts --- Notes.md | 7 + src/main/java/dev/wefhy/whymap/WhyMapMod.kt | 3 +- .../dev/wefhy/whymap/compose/ComposeView.kt | 25 +- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 286 +++++++++++++----- .../wefhy/whymap/compose/ui/WaypointsView.kt | 30 +- .../whymap/mixin/MainUIDispatcherMixin.java | 25 ++ .../dev/wefhy/whymap/tiles/region/MapArea.kt | 7 +- src/main/resources/whymap.mixins.json | 1 + 8 files changed, 277 insertions(+), 107 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/mixin/MainUIDispatcherMixin.java diff --git a/Notes.md b/Notes.md index f6500bd..0a0a8ce 100644 --- a/Notes.md +++ b/Notes.md @@ -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. diff --git a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt index 81b1947..3e6f52e 100644 --- a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt +++ b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt @@ -126,7 +126,8 @@ class WhyMapMod : ModInitializer { val message = Text.literal("WhyMap: see your map at ") + Text.literal(mapLink).apply { style = style.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, mapLink)).withUnderline(true) - } + } + Text.literal(" or press ${kbModSettings.boundKeyLocalizedText} to new in-game map!") +// } + Text.literal(" or press ${KeyBindingHelper.getBoundKeyOf(kbModSettings)} to open map") client.player!!.sendMessage(message, false) WorldEventQueue.addUpdate(WorldEventQueue.WorldEvent.EnterWorld) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt index c617071..169a2a9 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -13,13 +13,17 @@ import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.scene.SingleLayerComposeScene +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.io.Closeable import java.util.concurrent.Executors @@ -30,6 +34,10 @@ open class ComposeView( private val density: Density = Density(2f), 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 width by mutableStateOf(width) @@ -53,7 +61,10 @@ open class ComposeView( } //TODO use ImageComposeScene, seems more popular? - private val scene = SingleLayerComposeScene(coroutineContext = coroutineContext, density = density) { + 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 } @@ -115,6 +126,16 @@ open class ComposeView( height * screenScale ) 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()) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index e69a1b7..eab9d1f 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -5,19 +5,25 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.animation.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import dev.wefhy.whymap.WhyMapMod import dev.wefhy.whymap.compose.ComposeView import dev.wefhy.whymap.utils.Accessors.clientInstance @@ -50,19 +56,6 @@ class ConfigScreen : Screen(Text.of("Config")) { } } -// init { -// RenderThreadScope.launch { -// while (true) { -//// composeView.passLMBClick(148.8671875f, 43.724609375f) -// composeView.passLMBClick(191f, 90f) -// delay(random.nextLong(25, 50)) -//// composeView.passLMBRelease(148.8671875f, 43.724609375f) -// composeView.passLMBRelease(191f, 90f) -// delay(random.nextLong(50, 550)) -// } -// } -// } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { // super.render(context, mouseX, mouseY, delta) composeView.render(context, delta) @@ -117,80 +110,84 @@ private fun UI(vm: MapViewModel) { elevation = 20.dp, modifier = Modifier/*.padding(200.dp, 0.dp, 0.dp, 0.dp)*/.padding(8.dp) ) { Box { - Row(Modifier.padding(8.dp)) { - println("Recomposition ${i++}") - Column { - Text("Clicks: $clicks") - Button(onClick = { clicks++ }) { - Text("Click me!") - color = Color(0x7F777700) - } - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Show List") - Switch(checked = showList, onCheckedChange = { showList = it }) + Column { + var showDropDown by remember { mutableStateOf(false) } + TopAppBar({ + Text("WhyMap") + + //smaller text in cursive + Text("by wefhy", fontSize = 12.sp, modifier = Modifier.padding(4.dp).offset(0.dp, 4.dp), fontStyle = FontStyle.Italic) +// DimensionDropDown() + Spacer(Modifier.weight(1f)) + BetterDimensionDrop() + }, actions = { + IconButton( + onClick = { showDropDown = true }) { + Icon(Icons.Filled.MoreVert, null) + } - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Show Map") - Switch(checked = showMap, onCheckedChange = { showMap = it }) + DropdownMenu( + showDropDown, { showDropDown = false } + // offset = DpOffset((-102).dp, (-64).dp), + ) { + DropdownMenuItem(/*icon = { + Icon( + Icons.Filled.Home + ) + },*/ onClick = { + showDropDown = false + }) { Text(text = "Drop down item") } } - } -// if(showList) { -// LazyColumn { //TODO scrolling LazyColumn will cause race condition in Recomposer, broadcastFrameClock -// items(20) { -// Text("Item $it") -// } -// } -// } -// return@Row - - AnimatedVisibility( - showMap, - enter = expandIn(), - exit = shrinkOut() - ) { + }) + Row(Modifier.padding(8.dp)) { + println("Recomposition ${i++}") Column { - Text("Map") - MapTileView(LocalTileBlock(clientInstance.player!!.pos)) + Text("Clicks: $clicks") + Button(onClick = { clicks++ }) { + Text("Click me!") + color = Color(0x7F777700) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Show List") + Switch(checked = showList, onCheckedChange = { showList = it }) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Show Map") + Switch(checked = showMap, onCheckedChange = { showMap = it }) + } } -// MapTileView(LocalTileThumbnail(16383, 16384, TileZoom.ThumbnailZoom)) - } - - AnimatedVisibility( - showList, - enter = expandIn(), - exit = shrinkOut() - ) { - val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() - val entries = waypoints.mapIndexed { i, it -> - WaypointEntry( - name = it.name, distance = 0.0f, waypointId = i, date = Date(), waypointStatus = WaypointEntry.Status.NEW, waypointType = WaypointEntry.Type.SIGHTSEEING - ) + AnimatedVisibility( + showMap, + enter = expandIn(), + exit = shrinkOut() + ) { + MapTileView(LocalTileBlock(clientInstance.player!!.pos)) } - WaypointsView(entries) { - println("Refresh!") + + AnimatedVisibility( + showList, + enter = expandIn(), + exit = shrinkOut() + ) { + val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() + val entries = waypoints.mapIndexed { i, it -> + WaypointEntry( + name = it.name, + distance = 0.0f, + waypointId = i, + date = Date(), + waypointStatus = WaypointEntry.Status.NEW, + waypointType = WaypointEntry.Type.SIGHTSEEING + ) + } + WaypointsView(entries) { + println("Refresh!") + } } -// val rememberScrollState = rememberScrollState() -// Column(Modifier.scrollable(rememberScrollState, orientation = Orientation.Vertical)) { -// Column(Modifier.verticalScroll(rememberScrollState)) { -// for (it in 0..20) { -// val hovered = remember { mutableStateOf(false) } -// Text("Item $it", Modifier.background(if (hovered.value) Color.Gray else Color.Transparent).padding(8.dp).onPointerEvent( -// PointerEventType.Move) { -// hovered.value = true -// }) -// } - -// for (entry in entries) { -// WaypointEntryView(entry) -// } -// WaypointsView(entries) { -// println("Refresh!") -// } -// } } } - FloatingActionButton(onClick = { vm.isDarkTheme = !vm.isDarkTheme }, Modifier.align(Alignment.TopEnd).padding(8.dp)) { + FloatingActionButton(onClick = { vm.isDarkTheme = !vm.isDarkTheme }, Modifier.align(Alignment.BottomEnd).padding(8.dp)) { val im = if (vm.isDarkTheme) Icons.TwoTone.ModeNight else Icons.TwoTone.WbSunny Icon(im, contentDescription = "Theme") } @@ -198,6 +195,131 @@ private fun UI(vm: MapViewModel) { } } + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BetterDimensionDrop() { + val coffeeDrinks = arrayOf("OverWorld", "Nether", "End", "Neth/OW overlay") + var expanded by remember { mutableStateOf(false) } + var selectedText by remember { mutableStateOf(coffeeDrinks[0]) } + + Box( + modifier = Modifier +// .fillMaxWidth() + .padding(4.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + } + ) { +// TextField( +// value = selectedText, +// textStyle = MaterialTheme.typography.body1, +// onValueChange = {}, +// readOnly = true, +// trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, +// // modifier = Modifier.menuAnchor() TODO this will be required in material3 +// ) + val interactionSource = remember { MutableInteractionSource() } + BasicTextField( + textStyle = LocalTextStyle.current.copy(fontSize = 18.sp).merge( + TextStyle(color = TextFieldDefaults.textFieldColors().textColor(true).value) + ), + value = selectedText, + onValueChange = {}, +// textStyle = TextStyle.Default.copy(fontSize = 18.sp), + modifier = Modifier +// .background( +// color = colors.background, +// shape = TextFieldDefaults.TextFieldShape//RoundedCornerShape(13.dp) +// ) +// .indicatorLine( +// enabled = enabled, +// isError = false, +// interactionSource = interactionSource, +// colors = TextFieldDefaults.outlinedTextFieldColors(), +// focusedIndicatorLineThickness = 0.dp, //to hide the indicator line +// unfocusedIndicatorLineThickness = 0.dp //to hide the indicator line +// ) + .height(42.dp), + + +// modifier = Modifier +// .fillMaxWidth() +// .padding(8.dp) +// .border(1.dp, Color.Black) + ) { + TextFieldDefaults.OutlinedTextFieldDecorationBox( + value = selectedText, + enabled = true, + innerTextField = it, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + contentPadding = PaddingValues(16.dp, 0.dp, 0.dp, 0.dp), + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) } + ) + } + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + coffeeDrinks.forEach { item -> + DropdownMenuItem( + onClick = { + selectedText = item + expanded = false + } + ) { Text(text = item) } + } + } + } + } +} + +@Composable +fun DimensionDropDown() { + var expanded by remember { mutableStateOf(false) } + var selected by remember { mutableStateOf("OverWorld") } + + Box( + modifier = Modifier.fillMaxWidth() + .wrapContentSize(Alignment.TopEnd).border(1.dp, Color.Black) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = { expanded = !expanded }) { + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "More" + ) + } + Text(selected) + } + + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + onClick = { selected = "OverWorld"} + ) { Text("OverWorld") } + DropdownMenuItem( + onClick = { selected = "Nether"} + ) { Text("Nether") } + DropdownMenuItem( + onClick = { selected = "End"} + ) { Text("End") } + DropdownMenuItem( + onClick = { selected = "Neth/OW overlay"} + ) { Text("Neth/OW overlay") } + } + } +} + @Preview @Composable private fun preview() { diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index 4022352..aa3a4eb 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -5,9 +5,9 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text @@ -104,33 +104,21 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit) { val state = rememberPullRefreshState(refreshing, ::refresh) Box(Modifier.pullRefresh(state).clipToBounds()) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier.verticalScroll(scrollState).padding(8.dp), + LazyColumn( + modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), -// contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp) + contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp), ) { - for (waypoint in waypoints) { - Box { - WaypointEntryView(waypoint) - } + items(waypoints) { + WaypointEntryView(it) } } -// LazyColumn( -// modifier = Modifier.fillMaxSize(), //TODO removing this causes race condition instead of crash -// verticalArrangement = Arrangement.spacedBy(8.dp), -// contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp) -// ) { -// items(waypoints) { -// WaypointEntryView(it) -// } -// } PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) } } -val viewEntry = WaypointEntry( +private val viewEntry = WaypointEntry( name = "Hello", distance = 123.57f, waypointId = 2137, @@ -153,5 +141,5 @@ fun Preview() { fun Preview2() { WaypointsView( listOf(viewEntry, viewEntry, viewEntry) - ){} + ) {} } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/mixin/MainUIDispatcherMixin.java b/src/main/java/dev/wefhy/whymap/mixin/MainUIDispatcherMixin.java new file mode 100644 index 0000000..b29eb95 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/mixin/MainUIDispatcherMixin.java @@ -0,0 +1,25 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.mixin; + +//public class GlobalSnapshotManagerMixin { +//} + + +import dev.wefhy.whymap.utils.WhyDispatchers; +import kotlinx.coroutines.CoroutineDispatcher; +import org.jetbrains.skiko.MainUIDispatcher_awtKt; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = MainUIDispatcher_awtKt.class, remap = false) +public class MainUIDispatcherMixin { + @Inject(method = "getMainUIDispatcher", at = @At("HEAD"), cancellable = true) +//@Inject(method = "getMainUIDispatcher()Lkotlinx/coroutines/CoroutineDispatcher;", at = @At("HEAD"), cancellable = true) +//@Inject(method = "Lkotlinx/coroutines/CoroutineDispatcher;", at = @At("HEAD"), cancellable = true) + private static void getMainUIDispatcher(CallbackInfoReturnable cir) { + cir.setReturnValue(WhyDispatchers.INSTANCE.getMainDispatcher()); + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/tiles/region/MapArea.kt b/src/main/java/dev/wefhy/whymap/tiles/region/MapArea.kt index b06ad59..096f54b 100644 --- a/src/main/java/dev/wefhy/whymap/tiles/region/MapArea.kt +++ b/src/main/java/dev/wefhy/whymap/tiles/region/MapArea.kt @@ -68,7 +68,7 @@ class MapArea private constructor(val location: LocalTileRegion) { val biomeMap: Array = Array(storageTileBlocks) { ByteArray(storageTileBlocks) { 0 } } // at least 7 bits, possibly 8 val lightMap: Array = Array(storageTileBlocks) { ByteArray(storageTileBlocks) { 0 } } // at least 4 bits val depthMap: Array = Array(storageTileBlocks) { ByteArray(storageTileBlocks) { 0 } } // at least 8 bits -// val exists = Array(storageTileChunks) { Array(storageTileChunks) { false } } // 1 bit +// val exists = Array(storageTileChunks) { Array(storageTileChunks) { false } } // 1 bit TODO use timestamp! val file = currentWorld.getFile(location) val thumbnailFile = currentWorld.getThumbnailFile(location) @@ -508,6 +508,11 @@ class MapArea private constructor(val location: LocalTileRegion) { } } +// val renderedImage +// get() = remember(someHash) { +// 4 //TODO implement something like this that doesn't need a composable. +// } + private fun _renderWhyImage(): WhyTiledImage { var failCounter = 0 val image = WhyTiledImage.BuildForRegion { y, x -> diff --git a/src/main/resources/whymap.mixins.json b/src/main/resources/whymap.mixins.json index c0ef9fd..44068c9 100644 --- a/src/main/resources/whymap.mixins.json +++ b/src/main/resources/whymap.mixins.json @@ -4,6 +4,7 @@ "package": "dev.wefhy.whymap.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ + "MainUIDispatcherMixin", "PlayerEntityMixin", "ServerPlayerEntityMixin" ], From cb219f88a9fa843d1af932fbc5fc008bc2667b5d Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Wed, 12 Jun 2024 01:10:18 +0200 Subject: [PATCH 17/23] Render waypoints, clickable waypoints --- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 17 ++-- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 84 +++++++++++-------- .../wefhy/whymap/compose/ui/WaypointsView.kt | 42 +++++----- .../whymap/mixin/DebugRendererMixin.java | 22 +++++ .../wefhy/whymap/overlay/WaypointRenderer.kt | 25 ++++++ src/main/java/dev/wefhy/whymap/utils/Utils.kt | 3 +- .../dev/wefhy/whymap/waypoints/Waypoint.kt | 5 ++ .../dev/wefhy/whymap/waypoints/Waypoints.kt | 2 +- src/main/resources/whymap.mixins.json | 5 +- 9 files changed, 140 insertions(+), 65 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/mixin/DebugRendererMixin.java create mode 100644 src/main/java/dev/wefhy/whymap/overlay/WaypointRenderer.kt diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index eab9d1f..88b379b 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -157,12 +157,14 @@ private fun UI(vm: MapViewModel) { } } + var center by remember { mutableStateOf(clientInstance.player!!.pos) } + AnimatedVisibility( showMap, enter = expandIn(), exit = shrinkOut() ) { - MapTileView(LocalTileBlock(clientInstance.player!!.pos)) + MapTileView(LocalTileBlock(center)) } AnimatedVisibility( @@ -173,16 +175,17 @@ private fun UI(vm: MapViewModel) { val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() val entries = waypoints.mapIndexed { i, it -> WaypointEntry( - name = it.name, - distance = 0.0f, waypointId = i, - date = Date(), - waypointStatus = WaypointEntry.Status.NEW, - waypointType = WaypointEntry.Type.SIGHTSEEING + name = it.name, + distance = clientInstance?.player?.pos?.distanceTo(it.pos.toVec3d())?.toFloat() ?: 0f, + coords = it.pos, ) } - WaypointsView(entries) { + WaypointsView(entries, { println("Refresh!") + }) { + println("Clicked on ${it.name}, centering on ${it.coords}") + center = it.coords.toVec3d() } } } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 03fdecc..0c666dc 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -2,13 +2,18 @@ package dev.wefhy.whymap.compose.ui +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateOffsetAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.material.Card +import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -26,6 +31,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowPosition.PlatformDefault.x import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.compose.ui.ComposeConstants.scaleRange import dev.wefhy.whymap.compose.ui.ComposeUtils.toLocalTileBlock @@ -42,11 +48,19 @@ import kotlinx.coroutines.withContext @Composable fun MapTileView(startPosition: LocalTileBlock) { val scope = rememberCoroutineScope() + val target by animateOffsetAsState(startPosition.toOffset(), animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessMediumLow + ) + ) val tileRadius = 1 val nTiles = 3 var scale by remember { mutableStateOf(1f) } var center by remember { mutableStateOf(startPosition.toOffset()) } - val block by remember { derivedStateOf { center.toLocalTileBlock() }} //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) + remember(target) { + center = target + } + val block by remember { derivedStateOf { center.toLocalTileBlock() } } //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) val centerTile = block.parent(TileZoom.RegionZoom) val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius) val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius) @@ -65,11 +79,11 @@ fun MapTileView(startPosition: LocalTileBlock) { println("MapTileView LaunchedEffect, tile: $tile, index: $index") val image = withContext(WhyDispatchers.Render) { activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { - if (!isActive) return@getRegionForTilesRendering null.also { println("Cancel early 1")} + if (!isActive) return@getRegionForTilesRendering null.also { println("Cancel early 1") } renderWhyImageNow().imageBitmap } } - if (!isActive) return@LaunchedEffect Unit.also { println("Cancel early 2")} + if (!isActive) return@LaunchedEffect Unit.also { println("Cancel early 2") } image?.let { images[tile] = it } @@ -78,43 +92,44 @@ fun MapTileView(startPosition: LocalTileBlock) { } } - Card( - elevation = 8.dp - ) { + Column { + Card( + elevation = 8.dp + ) { // val dpSize = with(LocalDensity.current) { //// DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) // DpSize(image?.width?.toDp() ?: 1.dp, image?.height?.toDp() ?: 1.dp) // } - Canvas(modifier = Modifier - .size(DpSize(400.dp, 400.dp)) - .background(Color(0.1f, 0.1f, 0.1f)) - .clipToBounds() - .pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() - center -= dragAmount / scale + Canvas(modifier = Modifier + .size(DpSize(400.dp, 400.dp)) + .background(Color(0.1f, 0.1f, 0.1f)) + .clipToBounds() + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + center -= dragAmount / scale + } } - } - .onPointerEvent(PointerEventType.Scroll) { - val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } - scale = (scale * (1 + scrollDelta.y / 10)).coerceIn(scaleRange) - } - ) { -// drawRect(Color.Black, Offset(0f, 0f), Size(size.width, size.height)) - for (y in minTile.z .. maxTile.z) { - for (x in minTile.x .. maxTile.x) { - val tile = LocalTileRegion(x, y) - val image = images[tile] - val drawOffset = tile.getStart() - image?.let { im -> - scale(scale) { - translate(size.width / 2, size.height / 2) { - translate( - center.x, - center.y) { - if (scale > 1) { - drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) - } else { - drawImage(im, topLeft = drawOffset.toOffset()) + .onPointerEvent(PointerEventType.Scroll) { + val scrollDelta = it.changes.fold(Offset.Zero) { acc, c -> acc + c.scrollDelta } + scale = (scale * (1 + scrollDelta.y / 10)).coerceIn(scaleRange) + } + ) { + for (y in minTile.z..maxTile.z) { + for (x in minTile.x..maxTile.x) { + val tile = LocalTileRegion(x, y) + val image = images[tile] + val drawOffset = tile.getStart() + image?.let { im -> + scale(scale) { + translate(size.width / 2, size.height / 2) { + translate(-center.x, -center.y) { + if (scale > 1) { + drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) + } else { + drawImage(im, topLeft = drawOffset.toOffset()) + } } } } @@ -125,7 +140,6 @@ fun MapTileView(startPosition: LocalTileBlock) { } } } - @Composable fun CachedCanvas(modifier: Modifier = Modifier, block: DrawScope.() -> Unit) { Spacer(modifier = Modifier.fillMaxSize().drawWithCache { diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index aa3a4eb..f13af0c 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -3,10 +3,13 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.onClick import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi @@ -24,6 +27,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import dev.wefhy.whymap.waypoints.CoordXYZ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.text.SimpleDateFormat @@ -31,16 +35,13 @@ import java.util.* class WaypointEntry( + val waypointId: Int, val name: String, val distance: Float, - val waypointId: Int, - val date: Date, - val waypointStatus: Status, - val waypointType: Type + val coords: CoordXYZ, + val date: Date? = null, + val waypointType: Type? = null ) { - enum class Status { - NEW, REACHED, ARCHIVED - } enum class Type { SPAWN, DEATH, TODO, HOME, SIGHTSEEING @@ -48,9 +49,9 @@ class WaypointEntry( } @Composable -fun WaypointEntryView(waypointEntry: WaypointEntry) { +fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifier) { val dateFormatter = SimpleDateFormat("HH:mm, EEE, MMM d", Locale.getDefault()) - Card(modifier = Modifier.fillMaxWidth(), elevation = 8.dp) { + Card(modifier = modifier.fillMaxWidth(), elevation = 8.dp) { Box( Modifier .fillMaxWidth() @@ -68,18 +69,20 @@ fun WaypointEntryView(waypointEntry: WaypointEntry) { ) } } - Text(text = dateFormatter.format(waypointEntry.date), fontSize = 16.sp) - Text(text = waypointEntry.waypointId.toString(), color = Color.Gray, fontSize = 14.sp) + + Text(text = "${waypointEntry.waypointType ?: ""}", fontSize = 16.sp) + Text(text = waypointEntry.date?.let {dateFormatter.format(it)} ?: "Now", color = Color.Gray, fontSize = 14.sp) } + val c = waypointEntry.coords Text( - text = "${waypointEntry.waypointStatus}/${waypointEntry.waypointType}", + text = "${c.x}, ${c.y}, ${c.z}", modifier = Modifier .align(Alignment.BottomEnd) .clip(RoundedCornerShape(8.dp)) .background( Color.Blue ) - .padding(4.dp), + .padding(6.dp), color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 15.sp @@ -88,9 +91,9 @@ fun WaypointEntryView(waypointEntry: WaypointEntry) { } } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable -fun WaypointsView(waypoints: List, onRefresh: () -> Unit) { +fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick: (WaypointEntry) -> Unit = {}) { val refreshScope = rememberCoroutineScope() var refreshing by remember { mutableStateOf(false) } @@ -110,7 +113,7 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit) { contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp), ) { items(waypoints) { - WaypointEntryView(it) + WaypointEntryView(it, Modifier.clickable { onClick(it) }) } } @@ -119,12 +122,11 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit) { } private val viewEntry = WaypointEntry( + waypointId = 2137, name = "Hello", distance = 123.57f, - waypointId = 2137, date = Date(), - waypointStatus = WaypointEntry.Status.NEW, - waypointType = WaypointEntry.Type.TODO + coords = CoordXYZ(1, 2, 3), ) @@ -140,6 +142,6 @@ fun Preview() { @Composable fun Preview2() { WaypointsView( - listOf(viewEntry, viewEntry, viewEntry) + listOf(viewEntry, viewEntry, viewEntry), {} ) {} } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/mixin/DebugRendererMixin.java b/src/main/java/dev/wefhy/whymap/mixin/DebugRendererMixin.java new file mode 100644 index 0000000..2f0f6c9 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/mixin/DebugRendererMixin.java @@ -0,0 +1,22 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.mixin; + +import dev.wefhy.whymap.overlay.WaypointRenderer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.debug.DebugRenderer; +import net.minecraft.client.util.math.MatrixStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(DebugRenderer.class) +public abstract class DebugRendererMixin { + @Inject(method = "render", at = @At("RETURN")) + private void renderDebugRenderers(MatrixStack matrixStack, VertexConsumerProvider.Immediate vtx, + double cameraX, double cameraY, double cameraZ, CallbackInfo ci) + { + WaypointRenderer.INSTANCE.renderDebugRenderers(matrixStack, vtx, cameraX, cameraY, cameraZ); + } +} diff --git a/src/main/java/dev/wefhy/whymap/overlay/WaypointRenderer.kt b/src/main/java/dev/wefhy/whymap/overlay/WaypointRenderer.kt new file mode 100644 index 0000000..ffb4a84 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/overlay/WaypointRenderer.kt @@ -0,0 +1,25 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.overlay + +import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.debug.DebugRenderer +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.math.Vec3d + +object WaypointRenderer { + fun renderDebugRenderers(matrixStack: MatrixStack?, vertexConsumers: VertexConsumerProvider?, camX: Double, camY: Double, camZ: Double) { + val playerPos = Vec3d(camX, camY, camZ) + for (waypoint in activeWorld?.waypoints?.waypoints ?: emptyList()) { + var loc = waypoint.location.toVec3d() + val distance = loc.distanceTo(playerPos) + if (distance > 100) { + loc = playerPos.add(loc.subtract(playerPos).normalize().multiply(100.0)) + } + val size = 0.002f * distance.coerceAtMost(100.0).toFloat() + //todo parse color + DebugRenderer.drawString(matrixStack, vertexConsumers, "${waypoint.name}(${distance.toInt()}m)", loc.x, loc.y, loc.z, -1, size, true, 0.0f, true) + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/utils/Utils.kt b/src/main/java/dev/wefhy/whymap/utils/Utils.kt index c7c2236..2c41b40 100644 --- a/src/main/java/dev/wefhy/whymap/utils/Utils.kt +++ b/src/main/java/dev/wefhy/whymap/utils/Utils.kt @@ -31,7 +31,8 @@ const val _1_3 = 1f / 3 const val _1_2 = 1f / 2 const val bestHashConst = 92821 val rand = Random(0) - +inline val Double.d1 get() = roundToString(1) +inline val Double.d2 get() = roundToString(2) inline fun Double.roundToString(places: Int) = String.format("%.${places}f", this) inline fun Float.roundToString(places: Int) = String.format("%.${places}f", this) diff --git a/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt b/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt index cfc1824..9eda829 100644 --- a/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt +++ b/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt @@ -4,6 +4,7 @@ package dev.wefhy.whymap.waypoints import dev.wefhy.whymap.utils.CoordinateConversion import kotlinx.serialization.Serializable +import net.minecraft.util.math.Vec3d @Serializable class LocalWaypoint(var name: String, val location: CoordXYZ, var color: String? = null, var initials: String? = null, val isDeath: Boolean? = null, val isBed: Boolean? = null) { @@ -26,6 +27,10 @@ data class CoordXYZ(val x: Int, val y: Int, val z: Int) { return LatLng(deg.first, deg.second) } + fun toVec3d(): Vec3d { + return Vec3d(x.toDouble(), y.toDouble(), z.toDouble()) + } + fun toLatLngWithHalfBlockOffset(): LatLng { val deg = CoordinateConversion.coord2deg(x.toDouble() + 0.5, z.toDouble() + 0.5) return LatLng(deg.first, deg.second) diff --git a/src/main/java/dev/wefhy/whymap/waypoints/Waypoints.kt b/src/main/java/dev/wefhy/whymap/waypoints/Waypoints.kt index 21f164b..6049e5a 100644 --- a/src/main/java/dev/wefhy/whymap/waypoints/Waypoints.kt +++ b/src/main/java/dev/wefhy/whymap/waypoints/Waypoints.kt @@ -13,7 +13,7 @@ import net.minecraft.util.math.GlobalPos context(CurrentWorldProvider) class Waypoints { private val file = currentWorld.worldPath.resolve("waypoints.txt") - private val waypoints: MutableList = try { + val waypoints: MutableList = try { if (file.exists()) Json.decodeFromString(file.readText()) else mutableListOf() diff --git a/src/main/resources/whymap.mixins.json b/src/main/resources/whymap.mixins.json index 44068c9..1cceba6 100644 --- a/src/main/resources/whymap.mixins.json +++ b/src/main/resources/whymap.mixins.json @@ -10,5 +10,8 @@ ], "injectors": { "defaultRequire": 1 - } + }, + "client": [ + "DebugRendererMixin" + ] } \ No newline at end of file From 88efab805db47be86cc92f49f3e2d2fc718ac747 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Wed, 12 Jun 2024 18:41:04 +0200 Subject: [PATCH 18/23] Hover waypoints, save theme, animate map, draw waypoints on map, draw player on map --- build.gradle.kts | 2 + src/main/java/dev/wefhy/whymap/WhyMapMod.kt | 5 + .../wefhy/whymap/compose/ui/ConfigScreen.kt | 63 ++++++--- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 124 ++++++++++++++---- .../wefhy/whymap/compose/ui/MapViewModel.kt | 16 ++- .../wefhy/whymap/compose/ui/WaypointsView.kt | 52 ++++++-- .../dev/wefhy/whymap/config/UserSettings.kt | 6 + .../wefhy/whymap/config/WhyUserSettings.kt | 3 + .../dev/wefhy/whymap/waypoints/Waypoint.kt | 5 + 9 files changed, 213 insertions(+), 63 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f1cd18d..4c9c512 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -123,6 +123,8 @@ dependencies { 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")) diff --git a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt index 3e6f52e..fb143d7 100644 --- a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt +++ b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt @@ -2,6 +2,7 @@ package dev.wefhy.whymap +import dev.wefhy.whymap.WhyMapClient.Companion.kbModSettings import dev.wefhy.whymap.config.FileConfigManager import dev.wefhy.whymap.config.WhyMapConfig.DEV_VERSION import dev.wefhy.whymap.config.WhyMapConfig.mapLink @@ -31,7 +32,11 @@ class WhyMapMod : ModInitializer { val chunkLoadScope = CoroutineScope(WhyDispatchers.ChunkLoad) +// @OptIn(ExperimentalCoroutinesApi::class) override fun onInitialize() { +// DecoroutinatorRuntime.load() +// DebugProbes.enableCreationStackTraces = true +// DebugProbes.install() FileConfigManager.load() ClientChunkEvents.CHUNK_LOAD.register { cw, wc -> diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 88b379b..90e1dda 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -29,10 +29,12 @@ import dev.wefhy.whymap.compose.ComposeView import dev.wefhy.whymap.utils.Accessors.clientInstance import dev.wefhy.whymap.utils.Accessors.clientWindow import dev.wefhy.whymap.utils.LocalTileBlock +import dev.wefhy.whymap.utils.parseHex +import dev.wefhy.whymap.waypoints.OnlineWaypoint import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text -import java.util.* +import net.minecraft.util.math.Vec3d class ConfigScreen : Screen(Text.of("Config")) { @@ -44,7 +46,8 @@ class ConfigScreen : Screen(Text.of("Config")) { density = Density(3f) ) { var visible by remember { mutableStateOf(false) } - MaterialTheme(colors = if(vm.isDarkTheme) darkColors() else lightColors()) { //todo change theme according to minecraft day/night or real life + val isDarkTheme by vm.isDark.collectAsState() + MaterialTheme(colors = if(isDarkTheme) darkColors() else lightColors()) { //todo change theme according to minecraft day/night or real life LaunchedEffect(Unit) { visible = true } @@ -140,6 +143,10 @@ private fun UI(vm: MapViewModel) { } }) Row(Modifier.padding(8.dp)) { + var hovered by remember { mutableStateOf(null) } + var updateCount by remember { mutableStateOf(0) } + var center by remember { mutableStateOf(clientInstance.player?.pos ?: Vec3d.ZERO) } + val entries = remember { mutableStateListOf() } println("Recomposition ${i++}") Column { Text("Clicks: $clicks") @@ -155,16 +162,34 @@ private fun UI(vm: MapViewModel) { Text("Show Map") Switch(checked = showMap, onCheckedChange = { showMap = it }) } +// Text("Hovered: ${hovered?.name ?: "None"}") } - var center by remember { mutableStateOf(clientInstance.player!!.pos) } + remember { + val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() + entries.addAll(waypoints.mapIndexed { i, it -> + WaypointEntry( + waypointId = i, + name = it.name, + color = it.color?.let { + if (it.first() != '#') return@let null + Color(it.drop(1).toInt(16)).copy(alpha = 1f) + } ?: Color.Black, + distance = clientInstance?.player?.pos?.distanceTo(it.pos.toVec3d())?.toFloat() ?: 0f, + coords = it.pos, + ) + }) + } + + Spacer(Modifier.weight(0.000001f)) AnimatedVisibility( showMap, + Modifier.weight(1f), enter = expandIn(), exit = shrinkOut() ) { - MapTileView(LocalTileBlock(center)) + MapTileView(LocalTileBlock(center), entries, hovered, updateCount) } AnimatedVisibility( @@ -172,26 +197,27 @@ private fun UI(vm: MapViewModel) { enter = expandIn(), exit = shrinkOut() ) { - val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() - val entries = waypoints.mapIndexed { i, it -> - WaypointEntry( - waypointId = i, - name = it.name, - distance = clientInstance?.player?.pos?.distanceTo(it.pos.toVec3d())?.toFloat() ?: 0f, - coords = it.pos, - ) - } WaypointsView(entries, { println("Refresh!") - }) { + }, { println("Clicked on ${it.name}, centering on ${it.coords}") + showMap = true center = it.coords.toVec3d() - } + updateCount++ + }, { entry, hovering -> + println("Hovering over ${entry.name}, hovering: $hovering") + if (hovering) { + hovered = entry + } else { + if (hovered == entry) + hovered = null + } + }) } } } - FloatingActionButton(onClick = { vm.isDarkTheme = !vm.isDarkTheme }, Modifier.align(Alignment.BottomEnd).padding(8.dp)) { - val im = if (vm.isDarkTheme) Icons.TwoTone.ModeNight else Icons.TwoTone.WbSunny + FloatingActionButton(onClick = { vm.isDark.value = !vm.isDark.value }, Modifier.align(Alignment.BottomEnd).padding(8.dp)) { + val im = if (vm.isDark.value) Icons.TwoTone.ModeNight else Icons.TwoTone.WbSunny Icon(im, contentDescription = "Theme") } } @@ -327,8 +353,7 @@ fun DimensionDropDown() { @Composable private fun preview() { val vm = MapViewModel() - vm.isDarkTheme = true - MaterialTheme(colors = if(vm.isDarkTheme) darkColors() else lightColors()) { + MaterialTheme(colors = darkColors()) { Scaffold { UI(vm) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 0c666dc..9b8b8e1 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -7,31 +7,32 @@ import androidx.compose.animation.core.animateOffsetAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Card -import androidx.compose.material.Text +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Place import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.scale -import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.WindowPosition.PlatformDefault.x import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.compose.ui.ComposeConstants.scaleRange import dev.wefhy.whymap.compose.ui.ComposeUtils.toLocalTileBlock @@ -43,22 +44,34 @@ import dev.wefhy.whymap.utils.WhyDispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +enum class MapControl { + User, Target +} @OptIn(ExperimentalComposeUiApi::class) @Composable -fun MapTileView(startPosition: LocalTileBlock) { +fun MapTileView(startPosition: LocalTileBlock, waypoints: List = emptyList(), hovered: WaypointEntry?, updateCount: Int = 0) { + //TODO layers - on max zoom just color the tile if the region file exists + var mapControl by remember { mutableStateOf(MapControl.Target) } + var animationTarget by remember { mutableStateOf(startPosition) } + remember(updateCount) { + mapControl = MapControl.Target + animationTarget = startPosition + } val scope = rememberCoroutineScope() - val target by animateOffsetAsState(startPosition.toOffset(), animationSpec = spring( - dampingRatio = Spring.DampingRatioLowBouncy, - stiffness = Spring.StiffnessMediumLow + val animationCenter by animateOffsetAsState(animationTarget.toOffset(), animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessMediumLow + ) ) - ) - val tileRadius = 1 - val nTiles = 3 + val tileRadius = 2 // plus center tile + val nTiles = tileRadius * 2 + 1 var scale by remember { mutableStateOf(1f) } var center by remember { mutableStateOf(startPosition.toOffset()) } - remember(target) { - center = target + remember(animationCenter, mapControl) { + if (mapControl == MapControl.Target) { + center = animationCenter + } } val block by remember { derivedStateOf { center.toLocalTileBlock() } } //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) val centerTile = block.parent(TileZoom.RegionZoom) @@ -92,7 +105,7 @@ fun MapTileView(startPosition: LocalTileBlock) { } } - Column { + Box { Card( elevation = 8.dp ) { @@ -102,13 +115,16 @@ fun MapTileView(startPosition: LocalTileBlock) { // } Canvas(modifier = Modifier - .size(DpSize(400.dp, 400.dp)) +// .size(DpSize(400.dp, 400.dp)) + .fillMaxSize() .background(Color(0.1f, 0.1f, 0.1f)) .clipToBounds() .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() center -= dragAmount / scale + animationTarget = center.toLocalTileBlock() + mapControl = MapControl.User } } .onPointerEvent(PointerEventType.Scroll) { @@ -116,15 +132,15 @@ fun MapTileView(startPosition: LocalTileBlock) { scale = (scale * (1 + scrollDelta.y / 10)).coerceIn(scaleRange) } ) { - for (y in minTile.z..maxTile.z) { - for (x in minTile.x..maxTile.x) { - val tile = LocalTileRegion(x, y) - val image = images[tile] - val drawOffset = tile.getStart() - image?.let { im -> - scale(scale) { - translate(size.width / 2, size.height / 2) { - translate(-center.x, -center.y) { + scale(scale) { + translate(size.width / 2, size.height / 2) { + translate(-center.x, -center.y) { + for (y in minTile.z..maxTile.z) { + for (x in minTile.x..maxTile.x) { + val tile = LocalTileRegion(x, y) + val image = images[tile] + val drawOffset = tile.getStart() + image?.let { im -> if (scale > 1) { drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) } else { @@ -133,10 +149,60 @@ fun MapTileView(startPosition: LocalTileBlock) { } } } + waypoints.forEach { + val offset = it.coords.toLocalBlock().toOffset() + Offset(0.5f, 0.5f) + val size = if (it == hovered) 16f else 8f + drawCircle( + color = it.color, + radius = size / scale, + center = offset, + style = Fill + ) + if (it == hovered) { + drawCircle( + color = if (it.color.luminance() > 0.5f) Color.Black else Color.White, + radius = size / scale, + center = offset, + style = Stroke(4f / scale) + ) + } + } + val player = activeWorld?.player?: return@scale + val playerPos = player.pos + val playerYaw = player.yaw + val offset = Offset(playerPos.x.toFloat(), playerPos.z.toFloat()) + translate(offset.x, offset.y) { + rotate(playerYaw, pivot = Offset(0f, 0f)) { + drawPath( + path = Path().apply { + val size = 16f / scale + moveTo(0f, 0f) + lineTo(-size, -size) + lineTo(0f, 1.5f*size) + lineTo(size, -size) + close() + }, + color = Color.Red, + style = Fill + ) + } + } } } } } + //center icon + Icon( + imageVector = Icons.Default.Place, + contentDescription = "Center", + modifier = Modifier.align(Alignment.BottomStart).padding(8.dp).size(32.dp).clip( + CircleShape).clickable { + val player = activeWorld?.player?: return@clickable + val playerPos = player.pos + animationTarget = Offset(playerPos.x.toFloat(), playerPos.z.toFloat()).toLocalTileBlock() + mapControl = MapControl.Target + }.background(MaterialTheme.colors.background).padding(4.dp) + ) } } } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt index 9475b0f..4a93a56 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt @@ -2,10 +2,24 @@ package dev.wefhy.whymap.compose.ui +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import dev.wefhy.whymap.config.UserSettings +import dev.wefhy.whymap.config.WhyUserSettings +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch class MapViewModel { - var isDarkTheme by mutableStateOf(false) + var isDark = MutableStateFlow(WhyUserSettings.generalSettings.theme == UserSettings.Theme.DARK) + init { + GlobalScope.launch { + isDark.collectLatest { + WhyUserSettings.generalSettings.theme = if (it) UserSettings.Theme.DARK else UserSettings.Theme.LIGHT + } + } + } } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index f13af0c..da43048 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -3,26 +3,31 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.onClick import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.pointerMoveFilter import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -37,6 +42,7 @@ import java.util.* class WaypointEntry( val waypointId: Int, val name: String, + val color: Color, val distance: Float, val coords: CoordXYZ, val date: Date? = null, @@ -55,6 +61,8 @@ fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifie Box( Modifier .fillMaxWidth() + .background(waypointEntry.color.copy(alpha = 0.25f)) + .clipToBounds() .padding(4.dp) ) { Column(modifier = Modifier.padding(4.dp)) { @@ -62,7 +70,7 @@ fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifie Text(text = waypointEntry.name, fontWeight = FontWeight.Bold, fontSize = 20.sp) Box(modifier = Modifier.fillMaxWidth()) { Text( - text = "${waypointEntry.distance}m", + text = "${waypointEntry.waypointType ?: ""}", modifier = Modifier.align(Alignment.CenterEnd), fontStyle = FontStyle.Italic, fontSize = 17.sp @@ -70,7 +78,7 @@ fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifie } } - Text(text = "${waypointEntry.waypointType ?: ""}", fontSize = 16.sp) + Text(text = "${waypointEntry.distance}m", fontSize = 16.sp) Text(text = waypointEntry.date?.let {dateFormatter.format(it)} ?: "Now", color = Color.Gray, fontSize = 14.sp) } val c = waypointEntry.coords @@ -80,20 +88,29 @@ fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifie .align(Alignment.BottomEnd) .clip(RoundedCornerShape(8.dp)) .background( - Color.Blue + waypointEntry.color +// Color.Blue ) .padding(6.dp), - color = Color.White, + color = if (waypointEntry.color.luminance() > 0.5f) Color.Black else Color.White, fontWeight = FontWeight.SemiBold, fontSize = 15.sp ) + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(4.dp) + .clickable { /*TODO*/ } + ) } } } -@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Composable -fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick: (WaypointEntry) -> Unit = {}) { +fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick: (WaypointEntry) -> Unit = {}, onHover: (WaypointEntry, Boolean) -> Unit = {_, _ -> }) { val refreshScope = rememberCoroutineScope() var refreshing by remember { mutableStateOf(false) } @@ -108,12 +125,18 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick Box(Modifier.pullRefresh(state).clipToBounds()) { LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.width(270.dp), verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp), ) { - items(waypoints) { - WaypointEntryView(it, Modifier.clickable { onClick(it) }) + items(waypoints) { wp -> + WaypointEntryView(wp, Modifier.clickable { + onClick(wp) + }.onPointerEvent(PointerEventType.Enter) { + onHover(wp, true) + }.onPointerEvent(PointerEventType.Exit) { + onHover(wp, false) + }) } } @@ -124,6 +147,7 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick private val viewEntry = WaypointEntry( waypointId = 2137, name = "Hello", + color = Color.Red, distance = 123.57f, date = Date(), coords = CoordXYZ(1, 2, 3), @@ -143,5 +167,5 @@ fun Preview() { fun Preview2() { WaypointsView( listOf(viewEntry, viewEntry, viewEntry), {} - ) {} + ) } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/config/UserSettings.kt b/src/main/java/dev/wefhy/whymap/config/UserSettings.kt index f55b061..ce10402 100644 --- a/src/main/java/dev/wefhy/whymap/config/UserSettings.kt +++ b/src/main/java/dev/wefhy/whymap/config/UserSettings.kt @@ -16,6 +16,7 @@ data class UserSettings( // "tripwire", // "vine", // ), + var theme: Theme = Theme.LIGHT, var mapScale: Double = 1.0, var displayHud: Boolean = true, var minimapPosition: MinimapPosition = MinimapPosition.TOP_LEFT, @@ -39,6 +40,11 @@ data class UserSettings( DEBUG } + enum class Theme { + LIGHT, + DARK + } + companion object { fun help(): String = UserSettings::class.java.declaredFields .filter { it.type.isEnum } diff --git a/src/main/java/dev/wefhy/whymap/config/WhyUserSettings.kt b/src/main/java/dev/wefhy/whymap/config/WhyUserSettings.kt index d5b44b8..50ca9d0 100644 --- a/src/main/java/dev/wefhy/whymap/config/WhyUserSettings.kt +++ b/src/main/java/dev/wefhy/whymap/config/WhyUserSettings.kt @@ -20,6 +20,7 @@ object WhyUserSettings: WhySettings() { fun load(userSettings: UserSettings) { mapSettings.mapScale = userSettings.mapScale generalSettings.displayHud = userSettings.displayHud + generalSettings.theme = userSettings.theme mapSettings.minimapPosition = userSettings.minimapPosition mapSettings.minimapMode = userSettings.minimapMode serverSettings.exposeHttpApi = userSettings.exposeHttpApi @@ -30,6 +31,7 @@ object WhyUserSettings: WhySettings() { return UserSettings( mapScale = mapSettings.mapScale, displayHud = generalSettings.displayHud, + theme = generalSettings.theme, minimapPosition = mapSettings.minimapPosition, minimapMode = mapSettings.minimapMode, exposeHttpApi = serverSettings.exposeHttpApi, @@ -41,6 +43,7 @@ object WhyUserSettings: WhySettings() { class GeneralSettingsCategory : WhySettingsCategory("General") { var displayHud by SettingsEntry(true).addToggle("Display HUD") var hudColor by SettingsEntry(WhyColor.White).addColorPicker("HUD color") + var theme by SettingsEntry(UserSettings.Theme.LIGHT).addToggle("Theme") } class MapSettingsCategory: WhySettingsCategory("Map") { diff --git a/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt b/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt index 9eda829..1a4f0ba 100644 --- a/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt +++ b/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt @@ -3,6 +3,7 @@ package dev.wefhy.whymap.waypoints import dev.wefhy.whymap.utils.CoordinateConversion +import dev.wefhy.whymap.utils.LocalTileBlock import kotlinx.serialization.Serializable import net.minecraft.util.math.Vec3d @@ -31,6 +32,10 @@ data class CoordXYZ(val x: Int, val y: Int, val z: Int) { return Vec3d(x.toDouble(), y.toDouble(), z.toDouble()) } + fun toLocalBlock(): LocalTileBlock { + return LocalTileBlock(x, z) + } + fun toLatLngWithHalfBlockOffset(): LatLng { val deg = CoordinateConversion.coord2deg(x.toDouble() + 0.5, z.toDouble() + 0.5) return LatLng(deg.first, deg.second) From 03100393eb4a76fb70b34a148d51c6c269bbf8b9 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Thu, 13 Jun 2024 01:42:27 +0200 Subject: [PATCH 19/23] Add minecraft theme, waypoint animations, toolbar, handle keyboard events, filter, sort waypoints --- .../dev/wefhy/whymap/compose/ComposeView.kt | 54 ++++++++++ .../dev/wefhy/whymap/compose/styles/Colors.kt | 51 +++++++++ .../dev/wefhy/whymap/compose/styles/Fonts.kt | 36 +++++++ .../wefhy/whymap/compose/styles/McTheme.kt | 51 +++++++++ .../wefhy/whymap/compose/ui/ConfigScreen.kt | 41 ++++--- .../wefhy/whymap/compose/ui/WaypointsView.kt | 102 ++++++++++++++---- .../wefhy/whymap/compose/views/IconRadio.kt | 79 ++++++++++++++ src/main/java/dev/wefhy/whymap/utils/Utils.kt | 19 ++++ .../fonts/minecraft-font/MinecraftBold.otf | Bin 0 -> 11164 bytes .../minecraft-font/MinecraftBoldItalic.otf | Bin 0 -> 11772 bytes .../fonts/minecraft-font/MinecraftItalic.otf | Bin 0 -> 12100 bytes .../fonts/minecraft-font/MinecraftRegular.otf | Bin 0 -> 11016 bytes .../resources/fonts/minecraft-font/info.txt | 2 + 13 files changed, 401 insertions(+), 34 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/views/IconRadio.kt create mode 100644 src/main/resources/fonts/minecraft-font/MinecraftBold.otf create mode 100644 src/main/resources/fonts/minecraft-font/MinecraftBoldItalic.otf create mode 100644 src/main/resources/fonts/minecraft-font/MinecraftItalic.otf create mode 100644 src/main/resources/fonts/minecraft-font/MinecraftRegular.otf create mode 100644 src/main/resources/fonts/minecraft-font/info.txt diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt index 169a2a9..1792fd9 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -12,6 +12,9 @@ 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.scene.MultiLayerComposeScene import androidx.compose.ui.unit.Density @@ -24,8 +27,10 @@ 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( @@ -109,6 +114,55 @@ open class ComposeView( ) } +// 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 = action//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) + scene.sendKeyEvent(event)//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) { diff --git a/src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt b/src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt new file mode 100644 index 0000000..a5eac5f --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt @@ -0,0 +1,51 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.styles + +import androidx.compose.material.Colors +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.ui.graphics.Color + +val mcColors = Colors( + primary = Color(0xFFbbbb66), + primaryVariant = Color(0xFF66bb55), + secondary = Color(0xFF215c16), + secondaryVariant = Color(0xFF222200), + background = Color(0xFF182218), + surface = Color(0xFF182218), + error = Color(0xFFCF6679), + onPrimary = Color.Black, + onSecondary = Color.Black, + onBackground = Color.White, + onSurface = Color.White, + onError = Color.Black, + false +) + +val noctuaColors = lightColors( + primary = Color(0xFF551805), +// primaryVariant = Color(0xFF551805), + secondary = Color(0xFFccad8f), + secondaryVariant = Color(0xFFE7CEB5), +// background = Color(0xFFE7CEB5), +// surface = Color(0xFFE7CEB5), +// error = Color(0xFFCF6679), +) + + +//val darkColorsOriginal = Colors( +// primary = Color(0xFFBB86FC), +// primaryVariant = Color(0xFF3700B3), +// secondary = Color(0xFF03DAC6), +// secondaryVariant = secondary, +// background = Color(0xFF121212), +// surface = Color(0xFF121212), +// error = Color(0xFFCF6679), +// onPrimary = Color.Black, +// onSecondary = Color.Black, +// onBackground = Color.White, +// onSurface = Color.White, +// onError = Color.Black, +// false +//) \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt b/src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt new file mode 100644 index 0000000..05d3885 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt @@ -0,0 +1,36 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.styles + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.platform.Font + +object MinecraftFont { + val minecraftFontFamily = FontFamily( + Font( + resource = "fonts/minecraft-font/MinecraftRegular.otf", + style = FontStyle.Normal, + weight = FontWeight.Normal + ), + Font( + resource = "fonts/minecraft-font/MinecraftItalic.otf", + style = FontStyle.Italic, + weight = FontWeight.Normal + ), + Font( + resource = "fonts/minecraft-font/MinecraftBold.otf", + style = FontStyle.Normal, + weight = FontWeight.Bold + ), + Font( + resource = "fonts/minecraft-font/MinecraftBoldItalic.otf", + style = FontStyle.Italic, + weight = FontWeight.Bold + ) + ) + val background = Color(0xFF6E6E6E) + val shadow = Color(0xFF404040) +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt b/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt new file mode 100644 index 0000000..81b9c52 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt @@ -0,0 +1,51 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.styles + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Shapes +import androidx.compose.material.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun McTheme(colors: Colors, content: @Composable () -> Unit) { + MaterialTheme( + colors = colors, + typography = Typography( + button = TextStyle( + fontFamily = MinecraftFont.minecraftFontFamily, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + fontSize = 20.sp, + shadow = Shadow( + color = MinecraftFont.shadow, + offset = Offset(4f, 4f), + blurRadius = 0.5f + ) + ), + body1 = TextStyle( + fontFamily = MinecraftFont.minecraftFontFamily, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + fontSize = 16.sp + ) + ), + shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) + ), + content = content + ) +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 90e1dda..f0f747b 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -6,8 +6,10 @@ import androidx.compose.animation.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -26,11 +28,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.wefhy.whymap.WhyMapMod import dev.wefhy.whymap.compose.ComposeView +import dev.wefhy.whymap.compose.styles.McTheme +import dev.wefhy.whymap.compose.styles.mcColors +import dev.wefhy.whymap.compose.styles.noctuaColors import dev.wefhy.whymap.utils.Accessors.clientInstance import dev.wefhy.whymap.utils.Accessors.clientWindow import dev.wefhy.whymap.utils.LocalTileBlock -import dev.wefhy.whymap.utils.parseHex -import dev.wefhy.whymap.waypoints.OnlineWaypoint import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text @@ -47,7 +50,7 @@ class ConfigScreen : Screen(Text.of("Config")) { ) { var visible by remember { mutableStateOf(false) } val isDarkTheme by vm.isDark.collectAsState() - MaterialTheme(colors = if(isDarkTheme) darkColors() else lightColors()) { //todo change theme according to minecraft day/night or real life + McTheme(colors = if(isDarkTheme) mcColors else noctuaColors) { //todo change theme according to minecraft day/night or real life LaunchedEffect(Unit) { visible = true } @@ -93,6 +96,16 @@ class ConfigScreen : Screen(Text.of("Config")) { return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) } + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + composeView.passKeyPress(keyCode, scanCode, modifiers) + return super.keyPressed(keyCode, scanCode, modifiers) + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + composeView.passKeyRelease(keyCode, scanCode, modifiers) + return super.keyReleased(keyCode, scanCode, modifiers) + } + override fun close() { composeView.close() super.close() @@ -107,7 +120,7 @@ private fun UI(vm: MapViewModel) { var clicks by remember { mutableStateOf(0) } var color by remember { mutableStateOf(Color.Green) } var showList by remember { mutableStateOf(true) } - var showMap by remember { mutableStateOf(false) } + var showMap by remember { mutableStateOf(true) } Card( border = BorderStroke(1.dp, Color(0.05f, 0.1f, 0.2f)), elevation = 20.dp, modifier = Modifier/*.padding(200.dp, 0.dp, 0.dp, 0.dp)*/.padding(8.dp) @@ -120,9 +133,9 @@ private fun UI(vm: MapViewModel) { //smaller text in cursive Text("by wefhy", fontSize = 12.sp, modifier = Modifier.padding(4.dp).offset(0.dp, 4.dp), fontStyle = FontStyle.Italic) -// DimensionDropDown() - Spacer(Modifier.weight(1f)) - BetterDimensionDrop() + DimensionDropDown() +// Spacer(Modifier.weight(1f)) +// BetterDimensionDrop() }, actions = { IconButton( onClick = { showDropDown = true }) { @@ -166,7 +179,7 @@ private fun UI(vm: MapViewModel) { } remember { - val waypoints = WhyMapMod.activeWorld?.waypoints?.onlineWaypoints ?: emptyList() + val waypoints = WhyMapMod.activeWorld?.waypoints?.waypoints ?: emptyList() entries.addAll(waypoints.mapIndexed { i, it -> WaypointEntry( waypointId = i, @@ -175,8 +188,8 @@ private fun UI(vm: MapViewModel) { if (it.first() != '#') return@let null Color(it.drop(1).toInt(16)).copy(alpha = 1f) } ?: Color.Black, - distance = clientInstance?.player?.pos?.distanceTo(it.pos.toVec3d())?.toFloat() ?: 0f, - coords = it.pos, + distance = clientInstance?.player?.pos?.distanceTo(it.location.toVec3d())?.toFloat() ?: 0f, + coords = it.location, ) }) } @@ -316,15 +329,15 @@ fun DimensionDropDown() { Box( modifier = Modifier.fillMaxWidth() - .wrapContentSize(Alignment.TopEnd).border(1.dp, Color.Black) + .wrapContentSize(Alignment.TopEnd).border(1.dp, Color.Black, shape = RoundedCornerShape(4.dp)) ) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = { expanded = !expanded }) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { expanded = !expanded }.padding(8.dp)) { +// IconButton(onClick = { expanded = !expanded }) { Icon( imageVector = Icons.Default.ArrowDropDown, contentDescription = "More" ) - } +// } Text(selected) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index da43048..7be1310 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -8,33 +8,40 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.input.key.* import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent -import androidx.compose.ui.input.pointer.pointerMoveFilter import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.sun.org.apache.xalan.internal.lib.ExsltStrings.padding +import dev.wefhy.whymap.compose.views.SortingOptions +import dev.wefhy.whymap.utils.roundToString import dev.wefhy.whymap.waypoints.CoordXYZ import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import net.minecraft.advancement.criterion.InventoryChangedCriterion.Conditions.items +import java.awt.event.KeyEvent +import java.awt.event.KeyEvent.VK_BACK_SPACE import java.text.SimpleDateFormat import java.util.* @@ -78,7 +85,7 @@ fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifie } } - Text(text = "${waypointEntry.distance}m", fontSize = 16.sp) + Text(text = "${waypointEntry.distance.roundToString(0)}m", fontSize = 16.sp) Text(text = waypointEntry.date?.let {dateFormatter.format(it)} ?: "Now", color = Color.Gray, fontSize = 14.sp) } val c = waypointEntry.coords @@ -108,11 +115,31 @@ fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifie } } +enum class WaypointSorting(name: String) { + ALPHABETICAL("Alphabetical"), + DISTANCE("Distance to player"), + DATE("Date"), + LOCATION("Distance to map center") +} + @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Composable fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick: (WaypointEntry) -> Unit = {}, onHover: (WaypointEntry, Boolean) -> Unit = {_, _ -> }) { val refreshScope = rememberCoroutineScope() var refreshing by remember { mutableStateOf(false) } + var search by rememberSaveable { mutableStateOf("") } + var reverse by remember { mutableStateOf(false) } + var sorting by remember { mutableStateOf(WaypointSorting.ALPHABETICAL) } + val filtered = waypoints.filter { it.name.contains(search, ignoreCase = true) }.let { + when (sorting) { + WaypointSorting.ALPHABETICAL -> it.sortedBy { it.name } + WaypointSorting.DISTANCE -> it.sortedBy { it.distance } + WaypointSorting.DATE -> it.sortedBy { it.date } + WaypointSorting.LOCATION -> it.sortedBy { it.coords.toVec3d().length() } + } + }.let { + if (reverse) it.reversed() else it + } fun refresh() = refreshScope.launch { refreshing = true @@ -123,20 +150,55 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick val state = rememberPullRefreshState(refreshing, ::refresh) - Box(Modifier.pullRefresh(state).clipToBounds()) { - LazyColumn( - modifier = Modifier.width(270.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp), - ) { - items(waypoints) { wp -> - WaypointEntryView(wp, Modifier.clickable { - onClick(wp) - }.onPointerEvent(PointerEventType.Enter) { - onHover(wp, true) - }.onPointerEvent(PointerEventType.Exit) { - onHover(wp, false) - }) + Box(Modifier.pullRefresh(state).clipToBounds().onKeyEvent { + if (it.type == KeyEventType.KeyDown) { + if (it.key.nativeKeyCode == VK_BACK_SPACE || it.key.nativeKeyCode == KeyEvent.VK_DELETE || it.key.nativeKeyCode == KeyEvent.VK_JAPANESE_KATAKANA) { + search = search.dropLast(1) + } else { + val keyText = KeyEvent.getKeyText(it.key.nativeKeyCode) + if (keyText.length == 1) { + search += keyText + } + } + } + false + }) { + Column { + Row { + SortingOptions(sorting) { + sorting = it + } + + //reverse sorting + IconButton(onClick = { reverse = !reverse }) { + Icon( + imageVector = if (reverse) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = "Reverse", + modifier = Modifier.padding(8.dp).align(Alignment.CenterVertically) + ) + } + +// TextField( +// value = search, +// onValueChange = { search = it }, +// label = { Text("Search") }, +// modifier = Modifier.width(200.dp).padding(8.dp) +// ) + } + LazyColumn( + modifier = Modifier.width(270.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp), + ) { + items(filtered, key = { it.waypointId }) { wp -> + WaypointEntryView(wp, Modifier.clickable { + onClick(wp) + }.onPointerEvent(PointerEventType.Enter) { + onHover(wp, true) + }.onPointerEvent(PointerEventType.Exit) { + onHover(wp, false) + }.animateItemPlacement()) + } } } diff --git a/src/main/java/dev/wefhy/whymap/compose/views/IconRadio.kt b/src/main/java/dev/wefhy/whymap/compose/views/IconRadio.kt new file mode 100644 index 0000000..3b67851 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/views/IconRadio.kt @@ -0,0 +1,79 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.views + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.sharp.Edit +import androidx.compose.material.icons.sharp.Person +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import dev.wefhy.whymap.compose.ui.WaypointSorting + +@Composable +fun IconRadioButton( + selected: Boolean, + onSelected: () -> Unit, + icon: ImageVector, + modifier: Modifier = Modifier +) { + OutlinedButton( + onClick = onSelected, + border = BorderStroke( + width = if (selected) 2.dp else 1.dp, + color = if (selected) MaterialTheme.colors.primary else Color.Gray + ), + modifier = modifier.width(48.dp).height(48.dp) + ) { + Icon( + modifier = Modifier.requiredWidth(48.dp).requiredHeight(48.dp), + imageVector = icon, + contentDescription = null + ) + } +} + +@Composable +fun SortingOptions( + selectedOption: WaypointSorting, + onOptionSelected: (WaypointSorting) -> Unit +) { + Row(Modifier.padding(16.dp, 0.dp)) { + IconRadioButton( + selected = selectedOption == WaypointSorting.ALPHABETICAL, + onSelected = { onOptionSelected(WaypointSorting.ALPHABETICAL) }, + icon = Icons.Sharp.Edit + ) + IconRadioButton( + selected = selectedOption == WaypointSorting.DISTANCE, + onSelected = { onOptionSelected(WaypointSorting.DISTANCE) }, + icon = Icons.Sharp.Person + ) + IconRadioButton( + selected = selectedOption == WaypointSorting.DATE, + onSelected = { onOptionSelected(WaypointSorting.DATE) }, + icon = Icons.Default.DateRange + ) + IconRadioButton( + selected = selectedOption == WaypointSorting.LOCATION, + onSelected = { onOptionSelected(WaypointSorting.LOCATION) }, + icon = Icons.Default.LocationOn + ) + } +} + +@Preview +@Composable +fun Preview() { + SortingOptions(WaypointSorting.DATE) {} +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/utils/Utils.kt b/src/main/java/dev/wefhy/whymap/utils/Utils.kt index 2c41b40..d535e66 100644 --- a/src/main/java/dev/wefhy/whymap/utils/Utils.kt +++ b/src/main/java/dev/wefhy/whymap/utils/Utils.kt @@ -5,6 +5,7 @@ package dev.wefhy.whymap.utils +import androidx.compose.ui.input.key.* import dev.wefhy.whymap.config.WhyMapConfig.logsDateFormatter import dev.wefhy.whymap.config.WhyMapConfig.logsEntryTimeFormatter import dev.wefhy.whymap.config.WhyMapConfig.pathForbiddenCharacters @@ -44,6 +45,24 @@ internal inline fun Double.significant(places: Int) = String.format("%.${_signif internal inline fun Double.significantBy(max: Double, places: Int) = String.format("%.${max._significant(places)}f", this) +//fun KeyEvent.toCompose() = KeyEvent( +// nativeKeyEvent = InternalKeyEvent( +// key = Key( +// nativeKeyCode = keyCode, +// nativeKeyLocation = keyLocationForCompose +// ), +// type = when (id) { +// KEY_PRESSED -> KeyEventType.KeyDown +// KEY_RELEASED -> KeyEventType.KeyUp +// else -> KeyEventType.Unknown +// }, +// codePoint = keyChar.code, +// modifiers = toPointerKeyboardModifiers(), +// nativeEvent = this +// ) +//) + + fun BufferedImage.getAverageColor(): Int { // This can only average up to 128x128 textures without integer overflow!!! val bytes = (data.dataBuffer as DataBufferByte).data val length = bytes.size diff --git a/src/main/resources/fonts/minecraft-font/MinecraftBold.otf b/src/main/resources/fonts/minecraft-font/MinecraftBold.otf new file mode 100644 index 0000000000000000000000000000000000000000..87b124cfd139705dd9bd0891b9e230e69e8106e9 GIT binary patch literal 11164 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/fonts/minecraft-font/MinecraftBoldItalic.otf b/src/main/resources/fonts/minecraft-font/MinecraftBoldItalic.otf new file mode 100644 index 0000000000000000000000000000000000000000..1f74f388ab4442bd91fd8486adb4b74806fb6333 GIT binary patch literal 11772 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 B8~6YK literal 0 HcmV?d00001 diff --git a/src/main/resources/fonts/minecraft-font/MinecraftItalic.otf b/src/main/resources/fonts/minecraft-font/MinecraftItalic.otf new file mode 100644 index 0000000000000000000000000000000000000000..6801bd85f04b94ec93ae3950accfdc2583d368af GIT binary patch literal 12100 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/fonts/minecraft-font/info.txt b/src/main/resources/fonts/minecraft-font/info.txt new file mode 100644 index 0000000..5238883 --- /dev/null +++ b/src/main/resources/fonts/minecraft-font/info.txt @@ -0,0 +1,2 @@ +license: Public Domain +link: https://www.fontspace.com/minecraft-font-f28180 \ No newline at end of file From 1a2fcfa700235660310390ef7c6da4fb05a400ec Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Thu, 13 Jun 2024 18:30:40 +0200 Subject: [PATCH 20/23] Handle different detail levels, Add noctua theme, improve minecraft theme --- .../dev/wefhy/whymap/compose/styles/Colors.kt | 10 +- .../dev/wefhy/whymap/compose/styles/Fonts.kt | 3 +- .../wefhy/whymap/compose/styles/McTheme.kt | 27 ++++- .../whymap/compose/ui/ComposeConstants.kt | 2 +- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 23 ++-- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 100 ++++++++++++++---- .../dev/wefhy/whymap/config/WhyMapConfig.kt | 2 +- .../java/dev/wefhy/whymap/utils/MapTile.kt | 2 + 8 files changed, 125 insertions(+), 44 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt b/src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt index a5eac5f..3f1bca7 100644 --- a/src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt +++ b/src/main/java/dev/wefhy/whymap/compose/styles/Colors.kt @@ -11,9 +11,9 @@ val mcColors = Colors( primary = Color(0xFFbbbb66), primaryVariant = Color(0xFF66bb55), secondary = Color(0xFF215c16), - secondaryVariant = Color(0xFF222200), - background = Color(0xFF182218), - surface = Color(0xFF182218), + secondaryVariant = Color(0xFF215c16), + background = Color(0xFF282218), + surface = Color(0xFF181818), error = Color(0xFFCF6679), onPrimary = Color.Black, onSecondary = Color.Black, @@ -27,8 +27,8 @@ val noctuaColors = lightColors( primary = Color(0xFF551805), // primaryVariant = Color(0xFF551805), secondary = Color(0xFFccad8f), - secondaryVariant = Color(0xFFE7CEB5), -// background = Color(0xFFE7CEB5), + secondaryVariant = Color.White, + background = Color(0xFFE7CEB5), // surface = Color(0xFFE7CEB5), // error = Color(0xFFCF6679), ) diff --git a/src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt b/src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt index 05d3885..f1743d6 100644 --- a/src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt +++ b/src/main/java/dev/wefhy/whymap/compose/styles/Fonts.kt @@ -32,5 +32,6 @@ object MinecraftFont { ) ) val background = Color(0xFF6E6E6E) - val shadow = Color(0xFF404040) +// val shadow = Color(0xFF404040) + val shadow = Color.Black.copy(alpha = 0.5f) } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt b/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt index 81b9c52..2293845 100644 --- a/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt +++ b/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt @@ -10,6 +10,7 @@ import androidx.compose.material.Shapes import androidx.compose.material.Typography import androidx.compose.runtime.Composable import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -18,11 +19,25 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp + +private val defaultMcTextStyle = TextStyle( + fontFamily = MinecraftFont.minecraftFontFamily, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + shadow = Shadow( + color = MinecraftFont.shadow, + offset = Offset(4f, 4f), + blurRadius = 0.5f + ) +) + @Composable fun McTheme(colors: Colors, content: @Composable () -> Unit) { +// val shadowColor = if (isSystemInDarkTheme()) Color.Black.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.5f) MaterialTheme( colors = colors, typography = Typography( + defaultFontFamily = MinecraftFont.minecraftFontFamily, button = TextStyle( fontFamily = MinecraftFont.minecraftFontFamily, fontWeight = FontWeight.Normal, @@ -38,8 +53,16 @@ fun McTheme(colors: Colors, content: @Composable () -> Unit) { fontFamily = MinecraftFont.minecraftFontFamily, fontWeight = FontWeight.Normal, fontStyle = FontStyle.Normal, - fontSize = 16.sp - ) + fontSize = 16.sp, + shadow = Shadow( + color = MinecraftFont.shadow, + offset = Offset(4f, 4f), + blurRadius = 0.5f + ) + ), + h1 = defaultMcTextStyle, + caption = defaultMcTextStyle, + subtitle1 = defaultMcTextStyle, ), shapes = Shapes( small = RoundedCornerShape(4.dp), diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt index ee021d0..0fb3468 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ComposeConstants.kt @@ -3,7 +3,7 @@ package dev.wefhy.whymap.compose.ui object ComposeConstants { - const val minScale = 0.001f + const val minScale = 0.1f const val maxScale = 50f val scaleRange = minScale..maxScale } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index f0f747b..d1d638e 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -5,6 +5,7 @@ package dev.wefhy.whymap.compose.ui import androidx.compose.animation.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -50,7 +51,7 @@ class ConfigScreen : Screen(Text.of("Config")) { ) { var visible by remember { mutableStateOf(false) } val isDarkTheme by vm.isDark.collectAsState() - McTheme(colors = if(isDarkTheme) mcColors else noctuaColors) { //todo change theme according to minecraft day/night or real life + McTheme(colors = if (isDarkTheme) mcColors else noctuaColors) { //todo change theme according to minecraft day/night or real life LaunchedEffect(Unit) { visible = true } @@ -118,7 +119,6 @@ private var i = 0 @Composable private fun UI(vm: MapViewModel) { var clicks by remember { mutableStateOf(0) } - var color by remember { mutableStateOf(Color.Green) } var showList by remember { mutableStateOf(true) } var showMap by remember { mutableStateOf(true) } Card( @@ -126,7 +126,7 @@ private fun UI(vm: MapViewModel) { elevation = 20.dp, modifier = Modifier/*.padding(200.dp, 0.dp, 0.dp, 0.dp)*/.padding(8.dp) ) { Box { - Column { + Column(Modifier.background(MaterialTheme.colors.background)) { var showDropDown by remember { mutableStateOf(false) } TopAppBar({ Text("WhyMap") @@ -165,7 +165,6 @@ private fun UI(vm: MapViewModel) { Text("Clicks: $clicks") Button(onClick = { clicks++ }) { Text("Click me!") - color = Color(0x7F777700) } Row(verticalAlignment = Alignment.CenterVertically) { Text("Show List") @@ -333,10 +332,10 @@ fun DimensionDropDown() { ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { expanded = !expanded }.padding(8.dp)) { // IconButton(onClick = { expanded = !expanded }) { - Icon( - imageVector = Icons.Default.ArrowDropDown, - contentDescription = "More" - ) + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "More" + ) // } Text(selected) } @@ -347,16 +346,16 @@ fun DimensionDropDown() { onDismissRequest = { expanded = false } ) { DropdownMenuItem( - onClick = { selected = "OverWorld"} + onClick = { selected = "OverWorld"; expanded = false } ) { Text("OverWorld") } DropdownMenuItem( - onClick = { selected = "Nether"} + onClick = { selected = "Nether"; expanded = false } ) { Text("Nether") } DropdownMenuItem( - onClick = { selected = "End"} + onClick = { selected = "End"; expanded = false } ) { Text("End") } DropdownMenuItem( - onClick = { selected = "Neth/OW overlay"} + onClick = { selected = "Neth/OW overlay"; expanded = false } ) { Text("Neth/OW overlay") } } } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 9b8b8e1..5e770ab 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -32,22 +32,72 @@ import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.compose.ui.ComposeConstants.scaleRange import dev.wefhy.whymap.compose.ui.ComposeUtils.toLocalTileBlock import dev.wefhy.whymap.compose.ui.ComposeUtils.toOffset -import dev.wefhy.whymap.utils.LocalTileBlock -import dev.wefhy.whymap.utils.LocalTileRegion -import dev.wefhy.whymap.utils.TileZoom -import dev.wefhy.whymap.utils.WhyDispatchers +import dev.wefhy.whymap.config.WhyMapConfig.storageTileBlocks +import dev.wefhy.whymap.config.WhyMapConfig.tileResolution +import dev.wefhy.whymap.utils.* +import dev.wefhy.whymap.utils.ImageWriter.encodeJPEG +import dev.wefhy.whymap.utils.ImageWriter.encodePNG import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.Image +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.nio.IntBuffer +import javax.imageio.ImageIO enum class MapControl { User, Target } +private suspend fun renderDetail(tile: LocalTileChunk) = withContext(WhyDispatchers.Render) { + val bufferedImage = activeWorld?.experimentalTileGenerator?.getTile(tile.chunkPos) ?: return@withContext null //TODO render directly to compose canvas! + val stream = ByteArrayOutputStream() + stream.encodeJPEG(bufferedImage) + try { + Image.makeFromEncoded(stream.toByteArray()).toComposeImageBitmap() + } catch (e: Throwable) { + null + } +} +private suspend fun renderRegion(tile: LocalTileRegion) = withContext(WhyDispatchers.Render) { + activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { + if (!isActive) return@getRegionForTilesRendering null.also { println("Cancel early 1") } + renderWhyImageNow().imageBitmap + } +} +private suspend fun renderThumbnail(tile: LocalTileThumbnail) = withContext(WhyDispatchers.Render) { + activeWorld?.thumbnailsManager?.getThumbnail(tile)?.let { + try { + Image.makeFromEncoded(it.toByteArray()).toComposeImageBitmap() + } catch (e: Throwable) { + null + } + } +} +//private suspend inline fun render(tile: LocalTile): ImageBitmap? = when (T::class) { +// TileZoom.RegionZoom::class -> renderRegion(tile as LocalTileRegion) +// TileZoom.ThumbnailZoom::class -> renderThumbnail(tile as LocalTileThumbnail) +// TileZoom.ChunkZoom::class -> renderDetail(tile as LocalTileChunk) +// else -> throw IllegalArgumentException("Unsupported zoom level: ${T::class.simpleName}") +//} + +private suspend inline fun render(tile: LocalTile): ImageBitmap? = when (tile.zoom.zoom) { + TileZoom.RegionZoom.zoom -> renderRegion(tile as LocalTileRegion) + TileZoom.ThumbnailZoom.zoom -> renderThumbnail(tile as LocalTileThumbnail) + TileZoom.ChunkZoom.zoom -> renderDetail(tile as LocalTileChunk) + else -> throw IllegalArgumentException("Unsupported zoom level: ${T::class.simpleName}") +} + + + + @OptIn(ExperimentalComposeUiApi::class) @Composable fun MapTileView(startPosition: LocalTileBlock, waypoints: List = emptyList(), hovered: WaypointEntry?, updateCount: Int = 0) { @@ -64,8 +114,6 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = stiffness = Spring.StiffnessMediumLow ) ) - val tileRadius = 2 // plus center tile - val nTiles = tileRadius * 2 + 1 var scale by remember { mutableStateOf(1f) } var center by remember { mutableStateOf(startPosition.toOffset()) } remember(animationCenter, mapControl) { @@ -73,29 +121,34 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = center = animationCenter } } + val zoom = when { + scale < 0.5 -> TileZoom.ThumbnailZoom + scale < 16 -> TileZoom.RegionZoom + else -> TileZoom.ChunkZoom + } + val tileRadius = when(zoom) { + TileZoom.ChunkZoom -> 4 + else -> 2 + } + val nTiles = tileRadius * 2 + 1 val block by remember { derivedStateOf { center.toLocalTileBlock() } } //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) - val centerTile = block.parent(TileZoom.RegionZoom) - val minTile = centerTile - LocalTileRegion(tileRadius, tileRadius) - val maxTile = centerTile + LocalTileRegion(tileRadius, tileRadius) - val dontDispose = remember { mutableSetOf() } - val images = remember { mutableStateMapOf() } + val centerTile = block.parent(zoom) + val minTile = centerTile - LocalTile(tileRadius, tileRadius, zoom) + val maxTile = centerTile + LocalTile(tileRadius, tileRadius, zoom) + val dontDispose = remember { mutableSetOf>() } + val images = remember { mutableStateMapOf, ImageBitmap>() } dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } for (x in minTile.x..maxTile.x) { for (z in minTile.z..maxTile.z) { - val tile = LocalTileRegion(x, z) + val tile = LocalTile(x, z, zoom) if (tile in dontDispose) continue val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) LaunchedEffect(tile) { assert(tile !in images) // images.remove(tile) //TODO actually just return if already loaded. But this should be handled by dontDispose println("MapTileView LaunchedEffect, tile: $tile, index: $index") - val image = withContext(WhyDispatchers.Render) { - activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { - if (!isActive) return@getRegionForTilesRendering null.also { println("Cancel early 1") } - renderWhyImageNow().imageBitmap - } - } + val image = render(tile) if (!isActive) return@LaunchedEffect Unit.also { println("Cancel early 2") } image?.let { images[tile] = it @@ -132,19 +185,22 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = scale = (scale * (1 + scrollDelta.y / 10)).coerceIn(scaleRange) } ) { - scale(scale) { + scale(scale ) { translate(size.width / 2, size.height / 2) { translate(-center.x, -center.y) { for (y in minTile.z..maxTile.z) { for (x in minTile.x..maxTile.x) { - val tile = LocalTileRegion(x, y) + val tile = LocalTile(x, y, zoom) val image = images[tile] val drawOffset = tile.getStart() + val res = (tileResolution / zoom.scale).toInt() image?.let { im -> if (scale > 1) { - drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), filterQuality = FilterQuality.None) +// drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize((im.width / zoom.scale).toInt(), (im.height / zoom.scale).toInt()), filterQuality = FilterQuality.None) + drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize(res, res), filterQuality = FilterQuality.None) } else { - drawImage(im, topLeft = drawOffset.toOffset()) +// drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize((im.width / zoom.scale).toInt(), (im.height / zoom.scale).toInt()), filterQuality = FilterQuality.Low) + drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize(res, res), filterQuality = FilterQuality.Low) } } } diff --git a/src/main/java/dev/wefhy/whymap/config/WhyMapConfig.kt b/src/main/java/dev/wefhy/whymap/config/WhyMapConfig.kt index 83f4271..0461373 100644 --- a/src/main/java/dev/wefhy/whymap/config/WhyMapConfig.kt +++ b/src/main/java/dev/wefhy/whymap/config/WhyMapConfig.kt @@ -50,7 +50,7 @@ object WhyMapConfig { val nativeZoomLevel = blockZoom - storageTileLog //17 val tileResolution = storageTileBlocks //512 - val regionThumbnailResolution = tileResolution shr regionThumbnailScaleLog + val regionThumbnailResolution = tileResolution shr regionThumbnailScaleLog //128 val legacyMetadataSize = 16 // bytes val metadataSize = 64 // bytes diff --git a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt index 18e5b0c..2805ffa 100644 --- a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt @@ -7,6 +7,7 @@ import dev.wefhy.whymap.utils.TileZoom.* import net.minecraft.util.math.ChunkPos import net.minecraft.util.math.Vec3d import java.io.File +import kotlin.math.pow open class MapTile(val x: Int, val z: Int, val zoom: Z) where Z : TileZoom { @@ -196,6 +197,7 @@ sealed class TileZoom(val zoom: Int) { object ThumbnailZoom : TileZoom(WhyMapConfig.thumbnailZoom) val offset = 1 shl (zoom - 1) + val scale = 2.0.pow(zoom - WhyMapConfig.regionZoom) } fun File.resolve(tile: MapTile) = this From dd4f5e20cdac5e10d165dc3eded736bfc18d4924 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Thu, 13 Jun 2024 22:59:04 +0200 Subject: [PATCH 21/23] Change density, close with M key, change detail level with canvas size --- src/main/java/dev/wefhy/whymap/WhyMapMod.kt | 2 +- .../dev/wefhy/whymap/compose/ComposeView.kt | 2 +- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 8 ++- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 49 +++++++++++-------- .../java/dev/wefhy/whymap/utils/MapTile.kt | 17 +++++++ 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt index fb143d7..e979fca 100644 --- a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt +++ b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt @@ -91,7 +91,7 @@ class WhyMapMod : ModInitializer { @JvmStatic fun dimensionChangeListener(newDimension: DimensionType) { val newDimensionName = newDimension.serialize() - if (oldDimensionName == newDimensionName) return Unit.also { println("NOT CHANGED WORLD") } + if (oldDimensionName == newDimensionName) return Unit.also { println("NOT CHANGED WORLD (old = $oldDimensionName, new = $newDimensionName)") } println("CHANGED WORLD! old: $oldDimensionName, new: $newDimensionName") oldDimensionName = newDimensionName val tmpWorld = activeWorld diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt index 1792fd9..2f24900 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -36,7 +36,7 @@ import java.awt.event.KeyEvent as AwtKeyEvent open class ComposeView( width: Int, height: Int, - private val density: Density = Density(2f), + private val density: Density, private val content: @Composable () -> Unit ) : Closeable { @OptIn(ExperimentalCoroutinesApi::class) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index d1d638e..6e7e934 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import dev.wefhy.whymap.WhyMapClient.Companion.kbModSettings import dev.wefhy.whymap.WhyMapMod import dev.wefhy.whymap.compose.ComposeView import dev.wefhy.whymap.compose.styles.McTheme @@ -47,7 +48,7 @@ class ConfigScreen : Screen(Text.of("Config")) { private val composeView = ComposeView( width = clientWindow.width, height = clientWindow.height, - density = Density(3f) + density = Density(2f) ) { var visible by remember { mutableStateOf(false) } val isDarkTheme by vm.isDark.collectAsState() @@ -98,6 +99,11 @@ class ConfigScreen : Screen(Text.of("Config")) { } override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (kbModSettings.matchesKey(keyCode, scanCode)) { + println("Closing settings!") + close() + return true + } composeView.passKeyPress(keyCode, scanCode, modifiers) return super.keyPressed(keyCode, scanCode, modifiers) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 5e770ab..2535c9e 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -121,14 +121,26 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = center = animationCenter } } - val zoom = when { - scale < 0.5 -> TileZoom.ThumbnailZoom - scale < 16 -> TileZoom.RegionZoom - else -> TileZoom.ChunkZoom + var canvasSize by remember { mutableStateOf(Size.Zero) } + val zoom = remember(scale, canvasSize) { + val tilesPerCanvas = canvasSize.maxDimension / tileResolution.toFloat() + val tilesPerVisibleCanvas = tilesPerCanvas / scale / 5 //5 is number of tiles visible on the screen + when(tilesPerVisibleCanvas) { + in 0f..0.05f -> TileZoom.ChunkZoom + in 0.05f.. 1.05f -> TileZoom.RegionZoom + else -> TileZoom.ThumbnailZoom + } +// when { +// scale < 0.5 -> TileZoom.ThumbnailZoom +// scale < 16 -> TileZoom.RegionZoom +// else -> TileZoom.ChunkZoom +// } } val tileRadius = when(zoom) { TileZoom.ChunkZoom -> 4 - else -> 2 + TileZoom.RegionZoom -> 2 + TileZoom.ThumbnailZoom -> 3 + else -> 0 } val nTiles = tileRadius * 2 + 1 val block by remember { derivedStateOf { center.toLocalTileBlock() } } //startPosition - LocalTileBlock(offsetX.toInt(), offsetY.toInt()) @@ -137,24 +149,20 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = val maxTile = centerTile + LocalTile(tileRadius, tileRadius, zoom) val dontDispose = remember { mutableSetOf>() } val images = remember { mutableStateMapOf, ImageBitmap>() } - dontDispose.removeAll { it.x !in minTile.x..maxTile.x || it.z !in minTile.z..maxTile.z } - for (x in minTile.x..maxTile.x) { - for (z in minTile.z..maxTile.z) { - val tile = LocalTile(x, z, zoom) - if (tile in dontDispose) continue - val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) - LaunchedEffect(tile) { - assert(tile !in images) + for (tile in (minTile..maxTile).toList().sortedByDescending { it distanceTo centerTile }) { + if (tile in dontDispose) continue + val index = tile.z.mod(nTiles) * nTiles + tile.x.mod(nTiles) + LaunchedEffect(tile) { + assert(tile !in images) // images.remove(tile) //TODO actually just return if already loaded. But this should be handled by dontDispose - println("MapTileView LaunchedEffect, tile: $tile, index: $index") - val image = render(tile) - if (!isActive) return@LaunchedEffect Unit.also { println("Cancel early 2") } - image?.let { - images[tile] = it - } - dontDispose.add(tile) + println("MapTileView LaunchedEffect, tile: $tile, index: $index") + val image = render(tile) + if (!isActive) return@LaunchedEffect Unit.also { println("Cancel early 2") } + image?.let { + images[tile] = it } + dontDispose.add(tile) } } @@ -185,6 +193,7 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = scale = (scale * (1 + scrollDelta.y / 10)).coerceIn(scaleRange) } ) { + canvasSize = size scale(scale ) { translate(size.width / 2, size.height / 2) { translate(-center.x, -center.y) { diff --git a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt index 2805ffa..8ff1e9a 100644 --- a/src/main/java/dev/wefhy/whymap/utils/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/utils/MapTile.kt @@ -7,6 +7,7 @@ import dev.wefhy.whymap.utils.TileZoom.* import net.minecraft.util.math.ChunkPos import net.minecraft.util.math.Vec3d import java.io.File +import kotlin.math.hypot import kotlin.math.pow open class MapTile(val x: Int, val z: Int, val zoom: Z) where Z : TileZoom { @@ -183,6 +184,22 @@ open class LocalTile(val x: Int, val z: Int, val zoom: Z) { } override fun toString() = "LocalTile$zoom{x: $x, z: $z}" + + infix fun distanceTo(other: LocalTile): Double { + return hypot((x - other.x).toDouble(), (z - other.z).toDouble()) + } + + operator fun rangeTo(maxTile: LocalTile): Sequence> { + val xRange = x..maxTile.x + val zRange = z..maxTile.z + return sequence { + for (x in xRange) { + for (z in zRange) { + yield(LocalTile(x, z, zoom)) + } + } + } + } } val LocalTileChunk.chunkPos From 45aedca2711bdd865b06063107692872defc0657 Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Fri, 14 Jun 2024 19:49:43 +0200 Subject: [PATCH 22/23] Add/edit/delete waypoints, color selectors, handle native screen sizes (window scaling), improve detail rendering performance, fix possible memory leaks, handle keyboard input --- src/main/java/dev/wefhy/whymap/WhyMapMod.kt | 2 +- .../dev/wefhy/whymap/compose/ComposeView.kt | 35 ++--- .../whymap/compose/ui/AddEditWaypoint.kt | 80 ++++++++++ .../wefhy/whymap/compose/ui/ColorSelector.kt | 43 ++++++ .../wefhy/whymap/compose/ui/ComposeUtils.kt | 12 -- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 26 +++- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 73 ++++----- .../wefhy/whymap/compose/ui/MapViewModel.kt | 11 +- .../wefhy/whymap/compose/ui/WaypointsView.kt | 118 ++++++++------ .../compose/ui/WorkaroundTextFieldSimple.kt | 129 ++++++++++++++++ .../whymap/compose/utils/ComposeUtils.kt | 45 ++++++ .../whymap/compose/utils/StateAdapters.kt | 54 +++++++ .../compose/utils/WorkaroundInteractions.kt | 12 ++ .../utils/WorkaroundKeyEventRecognizer.kt | 47 ++++++ .../details/ExperimentalTextureProvider.kt | 18 +++ .../details/ExperimentalTileGenerator.kt | 146 +++++++++++++++++- .../dev/wefhy/whymap/waypoints/Waypoint.kt | 7 + 17 files changed, 717 insertions(+), 141 deletions(-) create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/ColorSelector.kt delete mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/utils/ComposeUtils.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/utils/StateAdapters.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt create mode 100644 src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt diff --git a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt index aac39b8..8fb6d89 100644 --- a/src/main/java/dev/wefhy/whymap/WhyMapMod.kt +++ b/src/main/java/dev/wefhy/whymap/WhyMapMod.kt @@ -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) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt index 2f24900..6b2baf5 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -16,6 +16,7 @@ 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 @@ -34,8 +35,8 @@ import java.awt.event.KeyEvent as AwtKeyEvent @OptIn(InternalComposeUiApi::class, ExperimentalComposeUiApi::class) open class ComposeView( - width: Int, - height: Int, + nativeWidth: Int, + nativeHeight: Int, private val density: Density, private val content: @Composable () -> Unit ) : Closeable { @@ -44,25 +45,19 @@ open class ComposeView( protected val singleThreadDispatcher = rawSingleThreadDispatcher + CoroutineExceptionHandler { _, throwable -> println(throwable) } private var invalidated = true - private val screenScale = 2 //TODO This is Macbook specific - private var width by mutableStateOf(width) - private var height by mutableStateOf(height) +// 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(width * screenScale, height * screenScale) + private val directRenderer: Renderer = DirectRenderer(nativeWidth, nativeHeight) private val boxedContent: @Composable () -> Unit get() = { -// val width by ::width.asFlow().collectAsState(0) -// val height by ::height.asFlow().collectAsState(0) -// with(LocalDensity.current) { //TODO this causes the crash lol xD -// val dpWidth = outputWidth.toDp() -// val dpHeight = outputHeight.toDp() -// println("DP: $dpWidth, $dpHeight") -// Box(Modifier.size(dpWidth, dpHeight)) { -// Box(Modifier.size(dpWidth, dpHeight).background(Color(0x77000077.toInt()))) { - Box(Modifier.size(width.dp * screenScale / density.density, height.dp * screenScale / density.density)) { + val(dpWidth, dpHeight) = with(LocalDensity.current) { + nativeWidth.toDp() to nativeHeight.toDp() + } + Box(Modifier.size(dpWidth, dpHeight)) { content() } -// } } //TODO use ImageComposeScene, seems more popular? @@ -173,11 +168,11 @@ open class ComposeView( // println("Cancelled rendering on thread ${Thread.currentThread().name}!") // isRendering = false // } - width = clientWindow.width - height = clientWindow.height + nativeWidth = clientWindow.framebufferWidth + nativeHeight = clientWindow.framebufferHeight directRenderer.onSizeChange( - width * screenScale, - height * screenScale + nativeWidth, + nativeHeight ) directRenderer.render(drawContext, tickDelta) { glCanvas -> /** diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt b/src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt new file mode 100644 index 0000000..a0ee1a3 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt @@ -0,0 +1,80 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld +import dev.wefhy.whymap.compose.utils.ComposeUtils.goodBackground +import dev.wefhy.whymap.waypoints.CoordXYZ +import dev.wefhy.whymap.waypoints.CoordXYZ.Companion.toCoordXYZ +import net.minecraft.client.MinecraftClient + +@Composable +fun AddEditWaypoint(original: WaypointEntry? = null, onDismiss: () -> Unit = {}) { + var waypoint by remember { mutableStateOf(original ?: WaypointEntry.new(0).copy(coords = MinecraftClient.getInstance()?.player?.pos?.toCoordXYZ() ?: CoordXYZ.ZERO)) } + Column(Modifier.background(waypoint.color.copy(alpha = 0.25f)).height(220.dp).padding(8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + var name by remember { mutableStateOf(waypoint.name) } + WorkaroundTextFieldSimple(name, { name = it }, Modifier.weight(1f), label = { Text("Name") }) +// WorkaroundTextFieldSimple(waypoint.name, { waypoint = waypoint.copy(name = it) }, Modifier.weight(1f), label = { Text("Name") }) + if (original != null) { + Icon(Icons.Default.Delete, contentDescription = "Delete", Modifier.clickable { + activeWorld?.waypoints?.remove(original.asOnlineWaypoint()) + onDismiss() + }.padding(8.dp)) + } + } + Row(Modifier.padding(0.dp, 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { + WorkaroundTextFieldSimple(waypoint.coords.x.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(x = it.toInt())) }, Modifier.weight(1f), label = { Text("X") }) + WorkaroundTextFieldSimple(waypoint.coords.y.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(y = it.toInt())) }, Modifier.weight(1f), label = { Text("Y") }) + WorkaroundTextFieldSimple(waypoint.coords.z.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(z = it.toInt())) }, Modifier.weight(1f), label = { Text("Z") }) + } + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + ColorSelector { waypoint = waypoint.copy(color = it) } + Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Button( + modifier = Modifier.weight(1f), + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error, contentColor = MaterialTheme.colors.onError) + ) { + Icon(Icons.Default.Clear, contentDescription = "Cancel") + } + Button( + modifier = Modifier.weight(1f), + onClick = { + if (original != null) { + activeWorld?.waypoints?.remove(original.asOnlineWaypoint()) + } + activeWorld?.waypoints?.add(waypoint.asOnlineWaypoint()) + onDismiss() + }, + colors = ButtonDefaults.buttonColors(backgroundColor = waypoint.color, contentColor = waypoint.color.goodBackground()) + ) { + Icon(Icons.Default.Check, contentDescription = "Add") + } + } + } + } +} + +@Preview +@Composable +private fun PreviewAddEditWaypoint() { + Column { + AddEditWaypoint() + AddEditWaypoint(WaypointEntry(12, "Test", Color.Red, 0f, CoordXYZ(0, 0, 0))) + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ColorSelector.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ColorSelector.kt new file mode 100644 index 0000000..a20d5c6 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ColorSelector.kt @@ -0,0 +1,43 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.material.Card +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import dev.wefhy.whymap.compose.utils.ComposeUtils + +@Composable +fun ColorSelector(modifier: Modifier = Modifier, onSelect: (Color) -> Unit) { + val availableColors = ComposeUtils.goodColors + Card(elevation = 8.dp, modifier = modifier) { + LazyHorizontalGrid(GridCells.Fixed(2), Modifier.padding(4.dp)) { + items(availableColors.size) { + ColorButton(color = availableColors[it], onSelect = onSelect) + } + } + } +} + +@Composable +fun ColorButton(color: Color, onSelect: (Color) -> Unit) { + Card(Modifier.requiredSize(40.dp, 40.dp).padding(4.dp).clickable { onSelect(color) }, elevation = 4.dp) { + Box(modifier = Modifier.background(color)) + } +} + +@Preview +@Composable +private fun ColorSelectorPreview() { + ColorSelector {} +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt deleted file mode 100644 index 2c14ba7..0000000 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ComposeUtils.kt +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2024 wefhy - -package dev.wefhy.whymap.compose.ui - -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.unit.toOffset -import dev.wefhy.whymap.utils.LocalTileBlock - -object ComposeUtils { - fun LocalTileBlock.toOffset(): Offset = androidx.compose.ui.unit.IntOffset(x, z).toOffset() - fun Offset.toLocalTileBlock(): LocalTileBlock = LocalTileBlock(x.toInt(), y.toInt()) -} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index 6e7e934..fda8340 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -33,9 +33,11 @@ import dev.wefhy.whymap.compose.ComposeView import dev.wefhy.whymap.compose.styles.McTheme import dev.wefhy.whymap.compose.styles.mcColors import dev.wefhy.whymap.compose.styles.noctuaColors +import dev.wefhy.whymap.compose.utils.collectAsMutableState import dev.wefhy.whymap.utils.Accessors.clientInstance import dev.wefhy.whymap.utils.Accessors.clientWindow import dev.wefhy.whymap.utils.LocalTileBlock +import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text @@ -43,13 +45,14 @@ import net.minecraft.util.math.Vec3d class ConfigScreen : Screen(Text.of("Config")) { - private val vm = MapViewModel() + private val composeView = ComposeView( - width = clientWindow.width, - height = clientWindow.height, + nativeWidth = clientWindow.framebufferWidth, + nativeHeight = clientWindow.framebufferWidth, density = Density(2f) ) { + val vm = MapViewModel(rememberCoroutineScope()) var visible by remember { mutableStateOf(false) } val isDarkTheme by vm.isDark.collectAsState() McTheme(colors = if (isDarkTheme) mcColors else noctuaColors) { //todo change theme according to minecraft day/night or real life @@ -66,11 +69,16 @@ class ConfigScreen : Screen(Text.of("Config")) { override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { // super.render(context, mouseX, mouseY, delta) +// println("SCREEN MATRIX: ${context.matrices.peek().positionMatrix}") + println("width: ${MinecraftClient.getInstance().window.width}, " + + "scaledWidth: ${MinecraftClient.getInstance().window.scaledWidth}, " + + "nativeWidth: ${MinecraftClient.getInstance().window.framebufferWidth}, " ) composeView.render(context, delta) } override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + // println("Mouse clicked at $mouseX, $mouseY") composeView.passLMBClick(mouseX.toFloat(), mouseY.toFloat()) return super.mouseClicked(mouseX, mouseY, button) @@ -127,6 +135,7 @@ private fun UI(vm: MapViewModel) { var clicks by remember { mutableStateOf(0) } var showList by remember { mutableStateOf(true) } var showMap by remember { mutableStateOf(true) } + var isDark by vm.isDark.collectAsMutableState() Card( border = BorderStroke(1.dp, Color(0.05f, 0.1f, 0.2f)), elevation = 20.dp, modifier = Modifier/*.padding(200.dp, 0.dp, 0.dp, 0.dp)*/.padding(8.dp) @@ -166,6 +175,7 @@ private fun UI(vm: MapViewModel) { var updateCount by remember { mutableStateOf(0) } var center by remember { mutableStateOf(clientInstance.player?.pos ?: Vec3d.ZERO) } val entries = remember { mutableStateListOf() } + var waypointRefresh by remember { mutableStateOf(0) } println("Recomposition ${i++}") Column { Text("Clicks: $clicks") @@ -183,8 +193,9 @@ private fun UI(vm: MapViewModel) { // Text("Hovered: ${hovered?.name ?: "None"}") } - remember { + remember(waypointRefresh) { val waypoints = WhyMapMod.activeWorld?.waypoints?.waypoints ?: emptyList() + entries.clear() entries.addAll(waypoints.mapIndexed { i, it -> WaypointEntry( waypointId = i, @@ -217,6 +228,7 @@ private fun UI(vm: MapViewModel) { ) { WaypointsView(entries, { println("Refresh!") + waypointRefresh++ }, { println("Clicked on ${it.name}, centering on ${it.coords}") showMap = true @@ -234,8 +246,8 @@ private fun UI(vm: MapViewModel) { } } } - FloatingActionButton(onClick = { vm.isDark.value = !vm.isDark.value }, Modifier.align(Alignment.BottomEnd).padding(8.dp)) { - val im = if (vm.isDark.value) Icons.TwoTone.ModeNight else Icons.TwoTone.WbSunny + FloatingActionButton(onClick = { isDark = !isDark }, Modifier.align(Alignment.BottomStart).padding(8.dp)) { + val im = if (isDark) Icons.TwoTone.ModeNight else Icons.TwoTone.WbSunny Icon(im, contentDescription = "Theme") } } @@ -370,7 +382,7 @@ fun DimensionDropDown() { @Preview @Composable private fun preview() { - val vm = MapViewModel() + val vm = MapViewModel(rememberCoroutineScope()) MaterialTheme(colors = darkColors()) { Scaffold { UI(vm) diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 2535c9e..465d3e4 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -30,41 +30,33 @@ import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowPosition.PlatformDefault.x +import androidx.compose.ui.window.WindowPosition.PlatformDefault.y import dev.wefhy.whymap.WhyMapMod.Companion.activeWorld import dev.wefhy.whymap.compose.ui.ComposeConstants.scaleRange -import dev.wefhy.whymap.compose.ui.ComposeUtils.toLocalTileBlock -import dev.wefhy.whymap.compose.ui.ComposeUtils.toOffset -import dev.wefhy.whymap.config.WhyMapConfig.storageTileBlocks +import dev.wefhy.whymap.compose.utils.ComposeUtils.goodBackground +import dev.wefhy.whymap.compose.utils.ComposeUtils.toImageBitmap +import dev.wefhy.whymap.compose.utils.ComposeUtils.toLocalTileBlock +import dev.wefhy.whymap.compose.utils.ComposeUtils.toOffset import dev.wefhy.whymap.config.WhyMapConfig.tileResolution import dev.wefhy.whymap.utils.* import dev.wefhy.whymap.utils.ImageWriter.encodeJPEG -import dev.wefhy.whymap.utils.ImageWriter.encodePNG import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -import org.jetbrains.skia.Bitmap import org.jetbrains.skia.Image -import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream -import java.nio.IntBuffer -import javax.imageio.ImageIO enum class MapControl { User, Target } private suspend fun renderDetail(tile: LocalTileChunk) = withContext(WhyDispatchers.Render) { - val bufferedImage = activeWorld?.experimentalTileGenerator?.getTile(tile.chunkPos) ?: return@withContext null //TODO render directly to compose canvas! - val stream = ByteArrayOutputStream() - stream.encodeJPEG(bufferedImage) - try { - Image.makeFromEncoded(stream.toByteArray()).toComposeImageBitmap() - } catch (e: Throwable) { - null - } +// val bufferedImage = activeWorld?.experimentalTileGenerator?.getTile(tile.chunkPos) ?: return@withContext null //TODO render directly to compose canvas! +// bufferedImage.toImageBitmap(intermediate = ImageFormat.JPEG) + activeWorld?.experimentalTileGenerator?.getComposeTile(tile) } private suspend fun renderRegion(tile: LocalTileRegion) = withContext(WhyDispatchers.Render) { activeWorld?.mapRegionManager?.getRegionForTilesRendering(tile) { @@ -108,7 +100,6 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = mapControl = MapControl.Target animationTarget = startPosition } - val scope = rememberCoroutineScope() val animationCenter by animateOffsetAsState(animationTarget.toOffset(), animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessMediumLow @@ -127,7 +118,7 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = val tilesPerVisibleCanvas = tilesPerCanvas / scale / 5 //5 is number of tiles visible on the screen when(tilesPerVisibleCanvas) { in 0f..0.05f -> TileZoom.ChunkZoom - in 0.05f.. 1.05f -> TileZoom.RegionZoom + in 0.05f.. 1.1f -> TileZoom.RegionZoom else -> TileZoom.ThumbnailZoom } // when { @@ -178,7 +169,8 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = Canvas(modifier = Modifier // .size(DpSize(400.dp, 400.dp)) .fillMaxSize() - .background(Color(0.1f, 0.1f, 0.1f)) +// .background(Color(0.1f, 0.1f, 0.1f)) + .background(Color.Black) .clipToBounds() .pointerInput(Unit) { detectDragGestures { change, dragAmount -> @@ -194,43 +186,34 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = } ) { canvasSize = size - scale(scale ) { + val filterQuality = if (scale > 1) FilterQuality.None else FilterQuality.Low + val res = (tileResolution / zoom.scale).toInt() + scale(scale) { translate(size.width / 2, size.height / 2) { translate(-center.x, -center.y) { - for (y in minTile.z..maxTile.z) { - for (x in minTile.x..maxTile.x) { - val tile = LocalTile(x, y, zoom) - val image = images[tile] - val drawOffset = tile.getStart() - val res = (tileResolution / zoom.scale).toInt() - image?.let { im -> - if (scale > 1) { -// drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize((im.width / zoom.scale).toInt(), (im.height / zoom.scale).toInt()), filterQuality = FilterQuality.None) - drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize(res, res), filterQuality = FilterQuality.None) - } else { -// drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize((im.width / zoom.scale).toInt(), (im.height / zoom.scale).toInt()), filterQuality = FilterQuality.Low) - drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize(res, res), filterQuality = FilterQuality.Low) - } - } + for (tile in minTile..maxTile) { + val image = images[tile] + val drawOffset = tile.getStart() + image?.let { im -> + drawImage(im, dstOffset = IntOffset(drawOffset.x, drawOffset.z), dstSize = IntSize(res, res), filterQuality = filterQuality) } } - waypoints.forEach { + waypoints.sortedBy { it == hovered }.forEach { val offset = it.coords.toLocalBlock().toOffset() + Offset(0.5f, 0.5f) val size = if (it == hovered) 16f else 8f + val outlineWidth = if (it == hovered) 5f else 2f drawCircle( color = it.color, radius = size / scale, center = offset, style = Fill ) - if (it == hovered) { - drawCircle( - color = if (it.color.luminance() > 0.5f) Color.Black else Color.White, - radius = size / scale, - center = offset, - style = Stroke(4f / scale) - ) - } + drawCircle( + color = it.color.goodBackground(), + radius = size / scale, + center = offset, + style = Stroke(outlineWidth / scale) + ) } val player = activeWorld?.player?: return@scale val playerPos = player.pos diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt index 4a93a56..6d1a43a 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapViewModel.kt @@ -2,21 +2,20 @@ package dev.wefhy.whymap.compose.ui -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import dev.wefhy.whymap.config.UserSettings import dev.wefhy.whymap.config.WhyUserSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -class MapViewModel { + +class MapViewModel(scope: CoroutineScope) { var isDark = MutableStateFlow(WhyUserSettings.generalSettings.theme == UserSettings.Theme.DARK) init { - GlobalScope.launch { + scope.launch { isDark.collectLatest { WhyUserSettings.generalSettings.theme = if (it) UserSettings.Theme.DARK else UserSettings.Theme.LIGHT } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index 7be1310..038dd28 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -2,6 +2,7 @@ package dev.wefhy.whymap.compose.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -10,9 +11,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.* import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -24,8 +23,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.key.* import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent @@ -35,8 +36,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sun.org.apache.xalan.internal.lib.ExsltStrings.padding import dev.wefhy.whymap.compose.views.SortingOptions +import dev.wefhy.whymap.utils.Accessors.clientInstance +import dev.wefhy.whymap.utils.rand import dev.wefhy.whymap.utils.roundToString import dev.wefhy.whymap.waypoints.CoordXYZ +import dev.wefhy.whymap.waypoints.CoordXYZ.Companion.toCoordXYZ +import dev.wefhy.whymap.waypoints.OnlineWaypoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.minecraft.advancement.criterion.InventoryChangedCriterion.Conditions.items @@ -46,7 +51,7 @@ import java.text.SimpleDateFormat import java.util.* -class WaypointEntry( +data class WaypointEntry( val waypointId: Int, val name: String, val color: Color, @@ -55,14 +60,28 @@ class WaypointEntry( val date: Date? = null, val waypointType: Type? = null ) { - + fun asOnlineWaypoint(): OnlineWaypoint { + return OnlineWaypoint(name, null, coords, "#${(color.toArgb() and 0xFFFFFF).toString(16)}") + } enum class Type { SPAWN, DEATH, TODO, HOME, SIGHTSEEING } + + companion object { + fun new(id: Int) = WaypointEntry( + waypointId = id, + name = "", + color = Color.White, + distance = 0f, + coords = CoordXYZ(0, 0, 0), + date = Date(), + waypointType = null + ) + } } @Composable -fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifier) { +fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifier, onEdit: () -> Unit = {}) { val dateFormatter = SimpleDateFormat("HH:mm, EEE, MMM d", Locale.getDefault()) Card(modifier = modifier.fillMaxWidth(), elevation = 8.dp) { Box( @@ -104,12 +123,12 @@ fun WaypointEntryView(waypointEntry: WaypointEntry, modifier: Modifier = Modifie fontSize = 15.sp ) Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete", + imageVector = Icons.Default.Edit, + contentDescription = "Edit", modifier = Modifier .align(Alignment.TopEnd) .padding(4.dp) - .clickable { /*TODO*/ } + .clickable { onEdit() } ) } } @@ -130,16 +149,22 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick var search by rememberSaveable { mutableStateOf("") } var reverse by remember { mutableStateOf(false) } var sorting by remember { mutableStateOf(WaypointSorting.ALPHABETICAL) } - val filtered = waypoints.filter { it.name.contains(search, ignoreCase = true) }.let { - when (sorting) { - WaypointSorting.ALPHABETICAL -> it.sortedBy { it.name } - WaypointSorting.DISTANCE -> it.sortedBy { it.distance } - WaypointSorting.DATE -> it.sortedBy { it.date } - WaypointSorting.LOCATION -> it.sortedBy { it.coords.toVec3d().length() } + val filtered by remember { + derivedStateOf { + waypoints.filter { it.name.contains(search, ignoreCase = true) }.let { + when (sorting) { + WaypointSorting.ALPHABETICAL -> it.sortedBy { it.name } + WaypointSorting.DISTANCE -> it.sortedBy { it.distance } + WaypointSorting.DATE -> it.sortedBy { it.date } + WaypointSorting.LOCATION -> it.sortedBy { it.coords.toVec3d().length() } + } + }.let { + if (reverse) it.reversed() else it + } } - }.let { - if (reverse) it.reversed() else it } + var addEditWaypoint by remember { mutableStateOf(false) } + var editedWaypoint by remember { mutableStateOf(null) } fun refresh() = refreshScope.launch { refreshing = true @@ -150,19 +175,7 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick val state = rememberPullRefreshState(refreshing, ::refresh) - Box(Modifier.pullRefresh(state).clipToBounds().onKeyEvent { - if (it.type == KeyEventType.KeyDown) { - if (it.key.nativeKeyCode == VK_BACK_SPACE || it.key.nativeKeyCode == KeyEvent.VK_DELETE || it.key.nativeKeyCode == KeyEvent.VK_JAPANESE_KATAKANA) { - search = search.dropLast(1) - } else { - val keyText = KeyEvent.getKeyText(it.key.nativeKeyCode) - if (keyText.length == 1) { - search += keyText - } - } - } - false - }) { + Box(Modifier.fillMaxHeight().pullRefresh(state).clipToBounds()) { Column { Row { SortingOptions(sorting) { @@ -186,7 +199,7 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick // ) } LazyColumn( - modifier = Modifier.width(270.dp), + modifier = Modifier.width(270.dp).weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(8.dp, 8.dp, 8.dp, 16.dp), ) { @@ -197,37 +210,50 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick onHover(wp, true) }.onPointerEvent(PointerEventType.Exit) { onHover(wp, false) - }.animateItemPlacement()) + }.animateItemPlacement()) { + editedWaypoint = wp + addEditWaypoint = true + } + } + item { + Spacer(modifier = Modifier.height(80.dp)) } } - } + AnimatedVisibility(visible = addEditWaypoint) { + Box(Modifier.width(272.dp)) { + AddEditWaypoint(editedWaypoint) { + editedWaypoint = null + addEditWaypoint = false + onRefresh() + } + } + } + } + AnimatedVisibility(visible = !addEditWaypoint, Modifier.align(Alignment.BottomEnd)) { + FloatingActionButton(onClick = { + addEditWaypoint = true + }, Modifier.padding(8.dp)) { + Icon(Icons.Default.Add, contentDescription = "Theme") + } + } PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) } } -private val viewEntry = WaypointEntry( - waypointId = 2137, +private fun viewEntry(id: Int) = WaypointEntry( + waypointId = id, name = "Hello", - color = Color.Red, + color = Color(rand.nextInt()), distance = 123.57f, date = Date(), coords = CoordXYZ(1, 2, 3), ) - -@Preview -@Composable -fun Preview() { - WaypointEntryView( - viewEntry - ) -} - @Preview @Composable fun Preview2() { WaypointsView( - listOf(viewEntry, viewEntry, viewEntry), {} + listOf(viewEntry(0), viewEntry(1), viewEntry(2)), {} ) } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt new file mode 100644 index 0000000..edcfa05 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt @@ -0,0 +1,129 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.material.TextFieldDefaults.indicatorLine +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.input.key.* +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import dev.wefhy.whymap.compose.utils.WorkaroundInteractions +import dev.wefhy.whymap.compose.utils.WorkaroundKeyEventRecognizer +import java.awt.event.KeyEvent.VK_BACK_SPACE + +@Composable +fun WorkaroundTextFieldSimple( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = TextFieldDefaults.TextFieldShape, + colors: TextFieldColors = TextFieldDefaults.textFieldColors() +) { + // If color is not provided via the text style, use content color as a default + val textColor = textStyle.color.takeOrElse { + colors.textColor(enabled).value + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + var textFieldValue by remember { mutableStateOf("") } + remember(value) { textFieldValue = value } + + val recognizer = remember { + WorkaroundKeyEventRecognizer(object : WorkaroundInteractions { + override fun onCharacterTyped(c: Char) { + onValueChange(textFieldValue + c) + } + + override fun onBackspace() { + onValueChange(textFieldValue.dropLast(1)) + } + + override fun onDelete() { + onValueChange(textFieldValue.drop(1)) + } + + override fun onEnter() { + //submit + } + + override fun onLeftArrow() { + + } + + override fun onRightArrow() { + + } + }) + } + + @OptIn(ExperimentalMaterialApi::class) + (BasicTextField( + value = value, + modifier = modifier + .background(colors.backgroundColor(enabled).value, shape) + .indicatorLine(enabled, isError, interactionSource, colors) + .defaultMinSize( + minWidth = TextFieldDefaults.MinHeight * 2, //TextFieldDefaults.MinWidth, + minHeight = TextFieldDefaults.MinHeight + ) + .onKeyEvent { + recognizer.onKeyEvent(it) + false + }, + onValueChange = {},//onValueChange, + enabled = enabled, + readOnly = readOnly, + textStyle = mergedTextStyle, + cursorBrush = SolidColor(colors.cursorColor(isError).value), + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + decorationBox = @Composable { innerTextField -> + // places leading icon, text field with label and placeholder, trailing icon + TextFieldDefaults.TextFieldDecorationBox( + value = value, + visualTransformation = visualTransformation, + innerTextField = innerTextField, + placeholder = placeholder, + label = label, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + singleLine = singleLine, + enabled = enabled, + isError = isError, + interactionSource = interactionSource, + colors = colors + ) + } + )) +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/utils/ComposeUtils.kt b/src/main/java/dev/wefhy/whymap/compose/utils/ComposeUtils.kt new file mode 100644 index 0000000..78a0fc3 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/utils/ComposeUtils.kt @@ -0,0 +1,45 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.utils + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.unit.toOffset +import dev.wefhy.whymap.utils.ImageFormat +import dev.wefhy.whymap.utils.ImageWriter.encode +import dev.wefhy.whymap.utils.ImageWriter.encodeJPEG +import dev.wefhy.whymap.utils.LocalTileBlock +import org.jetbrains.skia.Image +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream + +object ComposeUtils { + fun LocalTileBlock.toOffset(): Offset = androidx.compose.ui.unit.IntOffset(x, z).toOffset() + fun Offset.toLocalTileBlock(): LocalTileBlock = LocalTileBlock(x.toInt(), y.toInt()) + val goodColors = listOf( + Color(0xFF0000FF), + Color(0xFF00FF00), + Color(0xFFFF0000), + Color(0xFFFFFF00), + Color(0xFFFF00FF), + Color(0xFF00FFFF), + Color(0xFF000000), + Color(0xFFFFFFFF), + Color(0xFF808080), + Color(0xFF663300) + ) + fun Color.goodBackground(): Color = if (luminance() > 0.5f) Color.Black else Color.White + + fun BufferedImage.toImageBitmap(intermediate: ImageFormat): ImageBitmap? { + val stream = ByteArrayOutputStream() + stream.encode(this, intermediate) + return try { + Image.makeFromEncoded(stream.toByteArray()).toComposeImageBitmap() + } catch (e: Throwable) { + null + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/utils/StateAdapters.kt b/src/main/java/dev/wefhy/whymap/compose/utils/StateAdapters.kt new file mode 100644 index 0000000..c07f682 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/utils/StateAdapters.kt @@ -0,0 +1,54 @@ +// Copyright (c) 2024 wefhy + +@file:Suppress("NOTHING_TO_INLINE") + +package dev.wefhy.whymap.compose.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + + +class MutableStateAdapter( + private val state: State, + private val mutate: (T) -> Unit +) : MutableState { + + override var value: T + get() = state.value + set(value) { + mutate(value) + } + + override fun component1(): T = value + override fun component2(): (T) -> Unit = { value = it } +} + + +@Composable +inline fun MutableStateFlow.collectAsMutableState( + context: CoroutineContext = EmptyCoroutineContext +): MutableState = MutableStateAdapter( + state = collectAsState(context), + mutate = { value = it } +) + +//@Composable +//fun MutableStateFlow.collectAsMutableState( +// context: CoroutineContext = EmptyCoroutineContext +//): MutableState = MutableStateFlowWrapperState(this) +// +//private class MutableStateFlowWrapperState( +// private val state: MutableStateFlow +//) : MutableState { +// override var value: T +// get() = state.value +// set(value) { state.value = value } +// +// override fun component1(): T = value +// override fun component2(): (T) -> Unit = { value = it } +//} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt new file mode 100644 index 0000000..e914104 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt @@ -0,0 +1,12 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.utils + +interface WorkaroundInteractions { + fun onCharacterTyped(c: Char) + fun onBackspace() + fun onDelete() + fun onEnter() + fun onLeftArrow() + fun onRightArrow() +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt new file mode 100644 index 0000000..a26ce26 --- /dev/null +++ b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt @@ -0,0 +1,47 @@ +// Copyright (c) 2024 wefhy + +package dev.wefhy.whymap.compose.utils + +import androidx.compose.ui.input.key.* +import androidx.compose.ui.input.key.Key.Companion +import java.awt.event.KeyEvent as AwtKeyEvent + +class WorkaroundKeyEventRecognizer(val interactions: WorkaroundInteractions) { + + val letters = setOf( + Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, Key.H, Key.I, Key.J, Key.K, Key.L, Key.M, Key.N, Key.O, Key.P, Key.Q, Key.R, Key.S, Key.T, Key.U, Key.V, Key.W, Key.X, Key.Y, Key.Z + ) + val numbers = setOf( + Key.One, Key.Two, Key.Three, Key.Four, Key.Five, Key.Six, Key.Seven, Key.Eight, Key.Nine, Key.Zero + ) + val numpad = setOf( + Key.NumPad0, Key.NumPad1, Key.NumPad2, Key.NumPad3, Key.NumPad4, Key.NumPad5, Key.NumPad6, Key.NumPad7, Key.NumPad8, Key.NumPad9 + ) + val symbols = setOf( + Key.Spacebar, Key.Comma, Key.Period, Key.Semicolon, Key.Slash, Key.Minus, Key.Equals + ) + val special = setOf( + Key.Backspace, Key.Delete, Key.Enter, Key.DirectionLeft, Key.DirectionRight + ) + val modifiers = setOf( + Key.ShiftRight, Key.ShiftLeft, Key.CtrlRight, Key.CtrlLeft, Key.AltLeft, Key.AltRight + ) + + fun onKeyEvent(keyEvent: KeyEvent) { + if (keyEvent.type != KeyEventType.KeyDown) return + val key = keyEvent.key + when (key) { + Key.Backspace -> interactions.onBackspace() + Key.Delete -> interactions.onDelete() + Key.Enter -> interactions.onEnter() + Key.DirectionLeft -> interactions.onLeftArrow() + Key.DirectionRight -> interactions.onRightArrow() + in letters -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) + in numbers -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) + in numpad -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) + in symbols -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) + else -> { + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTextureProvider.kt b/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTextureProvider.kt index d18f353..5392c36 100644 --- a/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTextureProvider.kt +++ b/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTextureProvider.kt @@ -2,6 +2,10 @@ package dev.wefhy.whymap.tiles.details +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import dev.wefhy.whymap.compose.utils.ComposeUtils.toImageBitmap +import dev.wefhy.whymap.utils.ImageFormat import dev.wefhy.whymap.whygraphics.WhyTile import dev.wefhy.whymap.whygraphics.WhyTile.Companion.asWhyTile import net.minecraft.block.Block @@ -14,8 +18,10 @@ import kotlin.jvm.optionals.getOrNull object ExperimentalTextureProvider { val waterTexture by lazy { getBitmap("water")} //TODO move to separate file + val waterComposeTexture by lazy { getComposeTexture("water")!! } //TODO move to separate file private val loadedTextures = mutableMapOf?>() + private val loadedComposeTextures = mutableMapOf?>() private val classLoader = javaClass.classLoader private val loadedWhyTiles = mutableMapOf>() @@ -35,6 +41,18 @@ object ExperimentalTextureProvider { val missingTextures = mutableListOf() + fun getComposeTexture(block: Block): ImageBitmap? { + return getComposeTexture(block.translationKey.split('.').last()) + } + + fun getComposeTexture(name: String): ImageBitmap? { + return loadedComposeTextures.getOrPut(name) { +// Optional.of(getBitmap(name)?.toImageBitmap(ImageFormat.PNG) ?: return@getOrPut Optional.empty()) + Optional.of(getBitmap(name)?.toComposeImageBitmap() ?: return@getOrPut Optional.empty()) + //TODO check whether .toComposeImageBitmap() is faster, it has a weird comment in the source code + }?.getOrNull() + } + @OptIn(ExperimentalStdlibApi::class) fun getBitmap(name: String): BufferedImage? { // MinecraftClient.getInstance().resourceManager.getResource() diff --git a/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTileGenerator.kt b/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTileGenerator.kt index 105f98c..be4d684 100644 --- a/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTileGenerator.kt +++ b/src/main/java/dev/wefhy/whymap/tiles/details/ExperimentalTileGenerator.kt @@ -2,20 +2,23 @@ package dev.wefhy.whymap.tiles.details +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* import dev.wefhy.whymap.CurrentWorldProvider import dev.wefhy.whymap.WhyWorld import dev.wefhy.whymap.communication.quickaccess.BlockQuickAccess.foliageBlocksSet import dev.wefhy.whymap.communication.quickaccess.BlockQuickAccess.ignoreDepthTint import dev.wefhy.whymap.communication.quickaccess.BlockQuickAccess.waterBlocks import dev.wefhy.whymap.communication.quickaccess.BlockQuickAccess.waterLoggedBlocks +import dev.wefhy.whymap.tiles.details.ExperimentalTextureProvider.waterComposeTexture import dev.wefhy.whymap.tiles.details.ExperimentalTextureProvider.waterTexture import dev.wefhy.whymap.tiles.region.MapArea -import dev.wefhy.whymap.utils.LocalTile -import dev.wefhy.whymap.utils.RectArea -import dev.wefhy.whymap.utils.TileZoom -import dev.wefhy.whymap.utils.chunkPos +import dev.wefhy.whymap.utils.* import dev.wefhy.whymap.whygraphics.* import kotlinx.coroutines.withContext +import net.minecraft.block.Blocks import net.minecraft.util.math.ChunkPos import java.awt.Color import java.awt.Graphics2D @@ -28,6 +31,7 @@ context(CurrentWorldProvider) class ExperimentalTileGenerator { val renderedTiles = mutableMapOf>() + val renderedComposeTiles = mutableMapOf>() //TODO this map is never freed?! @OptIn(ExperimentalStdlibApi::class) @@ -36,6 +40,10 @@ class ExperimentalTileGenerator { Optional.ofNullable(renderTile(position)) }.getOrNull() + suspend fun getComposeTile(tile: LocalTileChunk): ImageBitmap? = renderedComposeTiles.getOrPut(tile) { + Optional.ofNullable(renderComposeTile(tile)) + }.getOrNull() + suspend fun MapArea.renderIntersection(g2d: Graphics2D, area: RectArea, offsetX: Int, offsetY: Int) { val chunks = ((area intersect location) ?: return).list() println("rendering ${chunks.size} chunks at $location, offset: $offsetX, $offsetY") @@ -53,6 +61,117 @@ class ExperimentalTileGenerator { }//.joinAll() } + private fun ColorMatrix.setOffset(floatArray: FloatArray) { + for (i in 0 until 4) { + this[i, 4] += floatArray[i] + } + } + + private fun ColorMatrix.setToScale(floatArray: FloatArray) { + for (i in 0 until 4) { + this[i, i] = floatArray[i] + } + } + + private fun MapArea.renderAt(canvas: Canvas, position: ChunkPos, offsetX: Int = 0, offsetY: Int = 0) { + val chunk = getChunk(position) ?: return + val chunkOverlay = getChunkOverlay(position) ?: return + val biomeFoliage = getChunkBiomeFoliageAndWater(position) ?: return + val depthmap = getChunkDepthmap(position) ?: return + val normalmap = getChunkNormals(position) ?: return + // val originalComposite = g2d.composite + // val alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f) + val drawSize = Size(16f, 16f) + for (y in 0 until 16) { + for (x in 0 until 16) { + val drawPosX = offsetX + x * 16 + val drawPosY = offsetY + y * 16 + val drawOffset = Offset(drawPosX.toFloat(), drawPosY.toFloat()) + val drawRect = Rect(drawOffset, drawSize) + val block = chunk[y][x] + val (foliageColor, oceanColor) = biomeFoliage[y][x] + val blockOverlay = chunkOverlay[y][x] + val depth = depthmap[y][x].toUByte().toInt() + val normalShade = normalmap[y][x].shade + + //TODO handle lava separately + + val blockColorFilter = + if (foliageBlocksSet.contains(block)) { + (foliageColor * normalShade).floatArray + } else { + normalShade.floatArray + } + val source = ExperimentalTextureProvider.getComposeTexture(block.block) + if (source != null) { + canvas.drawImage( //TODO java.lang.IllegalArgumentException: Rescaling cannot be performed on an indexed image + source, + drawOffset, + Paint().apply { colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { + setToScale(blockColorFilter[0], blockColorFilter[1], blockColorFilter[2], blockColorFilter[3]) + }) }, + ) + } else { + canvas.drawRect( + drawPosX.toFloat(), + drawPosY.toFloat(), + 16f, + 16f, + Paint().apply { color = androidx.compose.ui.graphics.Color(block.getMapColor(null, null).color) } + ) + } + if (depth == 0 || blockOverlay.isAir) continue + val sourceOverlay = ExperimentalTextureProvider.getComposeTexture(blockOverlay.block) + val tmp1 = (1 - depth * 0.02f).coerceAtLeast(0f) + val alpha = (3 - tmp1 * tmp1) / 3 + val darken = -depth * 1.6f + + val c = if (waterBlocks.contains(blockOverlay)) oceanColor + else if (foliageBlocksSet.contains(blockOverlay)) foliageColor + else WhyColor.White + + val darkenArray = if (!ignoreDepthTint.contains(blockOverlay)) { + floatArrayOf(darken, darken, darken, 0f) + } else FloatArray(4) + + val newRop = if (waterLoggedBlocks.contains(blockOverlay)) { +// val waterRop = RescaleOp(oceanColor.floatArray.apply { this[3] = alpha * 1.6f }, darkenArray, null) + val waterRop = ColorMatrix().apply { + setToScale(oceanColor.floatArray.apply { this[3] = alpha * 1.6f }) + setOffset(darkenArray) + } + canvas.drawImage( + waterComposeTexture, + drawOffset, + Paint().apply { colorFilter = ColorFilter.colorMatrix(waterRop) } + ) +// RescaleOp(c.floatArray, FloatArray(4), null) + val cc = c.floatArray + ColorMatrix().apply { setToScale(cc) } + } else { +// RescaleOp(c.floatArray.apply { this[3] = alpha * 1.6f }, darkenArray, null) //TODO don't change alpha for non-water! + ColorMatrix().apply { + setToScale(c.floatArray.apply { this[3] = alpha * 1.6f }) + setOffset(darkenArray) + } + } + + if (sourceOverlay != null) { + canvas.drawImage(sourceOverlay, drawOffset, Paint().apply { colorFilter = ColorFilter.colorMatrix(newRop) }) + } else { + canvas.drawRect( + drawRect, + Paint().apply { color = androidx.compose.ui.graphics.Color( + (c + (-depth * 4)).intRGB or ((alpha * 255).toInt() shl 24) //TODO use proper alpha! + ) } + ) + } + + //TODO refactor this rendering finally + } + } + } + private fun MapArea.renderAt(g2d: Graphics2D, position: ChunkPos, offsetX: Int = 0, offsetY: Int = 0) { val chunk = getChunk(position) ?: return val chunkOverlay = getChunkOverlay(position) ?: return @@ -129,6 +248,25 @@ class ExperimentalTileGenerator { } } + private suspend fun renderComposeTile(tile: LocalTileChunk): ImageBitmap? { + return try { + currentWorld.mapRegionManager.getRegionForTilesRendering( + tile.parent(TileZoom.RegionZoom) + ) { + val bitmap = ImageBitmap(16 * 16, 16 * 16) + val canvas = Canvas(bitmap) + renderAt(canvas, tile.chunkPos) + bitmap + } + } catch (e: IndexOutOfBoundsException) { + println(" Failed to render chunk (${tile.x}, ${tile.z}) due to out of bounds") + null + } catch (e: IllegalArgumentException) { + println("Failed to render chunk (${tile.x}, ${tile.z}) due do indexed image (probably)") + null + } + } + private suspend fun renderTile(position: ChunkPos): BufferedImage? { return try { currentWorld.mapRegionManager.getRegionForTilesRendering( diff --git a/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt b/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt index 1a4f0ba..29fe481 100644 --- a/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt +++ b/src/main/java/dev/wefhy/whymap/waypoints/Waypoint.kt @@ -44,4 +44,11 @@ data class CoordXYZ(val x: Int, val y: Int, val z: Int) { override fun toString(): String { return "(x=$x, y=$y, z=$z)" } + + companion object { + val ZERO = CoordXYZ(0, 0, 0) + fun Vec3d.toCoordXYZ(): CoordXYZ { + return CoordXYZ(x.toInt(), y.toInt(), z.toInt()) + } + } } \ No newline at end of file From 0325860e6b6de43d37597ee69b95d68f941abbdd Mon Sep 17 00:00:00 2001 From: wefhy <> Date: Fri, 14 Jun 2024 20:31:52 +0200 Subject: [PATCH 23/23] Fix waypoint add/edit, improve text fields, undo m key to close view, fix player position, remove text shadow from some places, improve keyboard support, fix compose restarts --- .../dev/wefhy/whymap/compose/ComposeView.kt | 14 ++-- .../wefhy/whymap/compose/styles/McTheme.kt | 32 ++++---- .../whymap/compose/ui/AddEditWaypoint.kt | 11 ++- .../wefhy/whymap/compose/ui/ConfigScreen.kt | 14 ++-- .../dev/wefhy/whymap/compose/ui/MapTile.kt | 11 +-- .../wefhy/whymap/compose/ui/WaypointsView.kt | 9 ++- .../compose/ui/WorkaroundTextFieldSimple.kt | 73 ++++++++++++++----- .../compose/utils/WorkaroundInteractions.kt | 21 ++++-- .../utils/WorkaroundKeyEventRecognizer.kt | 36 +++++++-- .../whymap/tiles/region/MapRegionManager.kt | 3 +- 10 files changed, 148 insertions(+), 76 deletions(-) diff --git a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt index 6b2baf5..22e713d 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ComposeView.kt @@ -139,18 +139,20 @@ open class ComposeView( 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 = action//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 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) + 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 { diff --git a/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt b/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt index 2293845..75f6d35 100644 --- a/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt +++ b/src/main/java/dev/wefhy/whymap/compose/styles/McTheme.kt @@ -31,6 +31,12 @@ private val defaultMcTextStyle = TextStyle( ) ) +private val mcStyleNoShadow = TextStyle( + fontFamily = MinecraftFont.minecraftFontFamily, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal +) + @Composable fun McTheme(colors: Colors, content: @Composable () -> Unit) { // val shadowColor = if (isSystemInDarkTheme()) Color.Black.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.5f) @@ -49,20 +55,20 @@ fun McTheme(colors: Colors, content: @Composable () -> Unit) { blurRadius = 0.5f ) ), - body1 = TextStyle( - fontFamily = MinecraftFont.minecraftFontFamily, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - fontSize = 16.sp, - shadow = Shadow( - color = MinecraftFont.shadow, - offset = Offset(4f, 4f), - blurRadius = 0.5f - ) - ), +// body1 = TextStyle( +// fontFamily = MinecraftFont.minecraftFontFamily, +// fontWeight = FontWeight.Normal, +// fontStyle = FontStyle.Normal, +// fontSize = 16.sp, +// shadow = Shadow( +// color = MinecraftFont.shadow, +// offset = Offset(4f, 4f), +// blurRadius = 0.5f +// ) +// ), h1 = defaultMcTextStyle, - caption = defaultMcTextStyle, - subtitle1 = defaultMcTextStyle, + caption = mcStyleNoShadow, + subtitle1 = mcStyleNoShadow, ), shapes = Shapes( small = RoundedCornerShape(4.dp), diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt b/src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt index a0ee1a3..53ef327 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/AddEditWaypoint.kt @@ -24,11 +24,10 @@ import net.minecraft.client.MinecraftClient @Composable fun AddEditWaypoint(original: WaypointEntry? = null, onDismiss: () -> Unit = {}) { - var waypoint by remember { mutableStateOf(original ?: WaypointEntry.new(0).copy(coords = MinecraftClient.getInstance()?.player?.pos?.toCoordXYZ() ?: CoordXYZ.ZERO)) } + var waypoint by remember(original) { mutableStateOf(original ?: WaypointEntry.new(0).copy(coords = MinecraftClient.getInstance()?.player?.pos?.toCoordXYZ() ?: CoordXYZ.ZERO)) } Column(Modifier.background(waypoint.color.copy(alpha = 0.25f)).height(220.dp).padding(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - var name by remember { mutableStateOf(waypoint.name) } - WorkaroundTextFieldSimple(name, { name = it }, Modifier.weight(1f), label = { Text("Name") }) + WorkaroundTextFieldSimple(waypoint.name, { waypoint = waypoint.copy(name = it) }, Modifier.weight(1f), label = { Text("Name") }) // WorkaroundTextFieldSimple(waypoint.name, { waypoint = waypoint.copy(name = it) }, Modifier.weight(1f), label = { Text("Name") }) if (original != null) { Icon(Icons.Default.Delete, contentDescription = "Delete", Modifier.clickable { @@ -38,9 +37,9 @@ fun AddEditWaypoint(original: WaypointEntry? = null, onDismiss: () -> Unit = {}) } } Row(Modifier.padding(0.dp, 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { - WorkaroundTextFieldSimple(waypoint.coords.x.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(x = it.toInt())) }, Modifier.weight(1f), label = { Text("X") }) - WorkaroundTextFieldSimple(waypoint.coords.y.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(y = it.toInt())) }, Modifier.weight(1f), label = { Text("Y") }) - WorkaroundTextFieldSimple(waypoint.coords.z.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(z = it.toInt())) }, Modifier.weight(1f), label = { Text("Z") }) + WorkaroundTextFieldSimple(waypoint.coords.x.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(x = it.toIntOrNull() ?: 0)) }, Modifier.weight(1f), label = { Text("X") }) + WorkaroundTextFieldSimple(waypoint.coords.y.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(y = it.toIntOrNull() ?: 0)) }, Modifier.weight(1f), label = { Text("Y") }) + WorkaroundTextFieldSimple(waypoint.coords.z.toString(), { waypoint = waypoint.copy(coords = waypoint.coords.copy(z = it.toIntOrNull() ?: 0)) }, Modifier.weight(1f), label = { Text("Z") }) } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { ColorSelector { waypoint = waypoint.copy(color = it) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt index fda8340..ad3b04c 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/ConfigScreen.kt @@ -69,10 +69,6 @@ class ConfigScreen : Screen(Text.of("Config")) { override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { // super.render(context, mouseX, mouseY, delta) -// println("SCREEN MATRIX: ${context.matrices.peek().positionMatrix}") - println("width: ${MinecraftClient.getInstance().window.width}, " + - "scaledWidth: ${MinecraftClient.getInstance().window.scaledWidth}, " + - "nativeWidth: ${MinecraftClient.getInstance().window.framebufferWidth}, " ) composeView.render(context, delta) } @@ -107,11 +103,11 @@ class ConfigScreen : Screen(Text.of("Config")) { } override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - if (kbModSettings.matchesKey(keyCode, scanCode)) { - println("Closing settings!") - close() - return true - } +// if (kbModSettings.matchesKey(keyCode, scanCode)) { +// println("Closing settings!") +// close() +// return true +// } composeView.passKeyPress(keyCode, scanCode, modifiers) return super.keyPressed(keyCode, scanCode, modifiers) } diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt index 465d3e4..63ecfd0 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/MapTile.kt @@ -43,6 +43,7 @@ import dev.wefhy.whymap.compose.utils.ComposeUtils.toLocalTileBlock import dev.wefhy.whymap.compose.utils.ComposeUtils.toOffset import dev.wefhy.whymap.config.WhyMapConfig.tileResolution import dev.wefhy.whymap.utils.* +import dev.wefhy.whymap.utils.Accessors.clientInstance import dev.wefhy.whymap.utils.ImageWriter.encodeJPEG import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext @@ -161,13 +162,7 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = Card( elevation = 8.dp ) { -// val dpSize = with(LocalDensity.current) { -//// DpSize(t?.width?.toDp() ?: 1.dp, t?.height?.toDp() ?: 1.dp) -// DpSize(image?.width?.toDp() ?: 1.dp, image?.height?.toDp() ?: 1.dp) -// } - Canvas(modifier = Modifier -// .size(DpSize(400.dp, 400.dp)) .fillMaxSize() // .background(Color(0.1f, 0.1f, 0.1f)) .background(Color.Black) @@ -215,7 +210,7 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = style = Stroke(outlineWidth / scale) ) } - val player = activeWorld?.player?: return@scale + val player = clientInstance?.player?: return@scale val playerPos = player.pos val playerYaw = player.yaw val offset = Offset(playerPos.x.toFloat(), playerPos.z.toFloat()) @@ -245,7 +240,7 @@ fun MapTileView(startPosition: LocalTileBlock, waypoints: List = contentDescription = "Center", modifier = Modifier.align(Alignment.BottomStart).padding(8.dp).size(32.dp).clip( CircleShape).clickable { - val player = activeWorld?.player?: return@clickable + val player = clientInstance?.player?: return@clickable val playerPos = player.pos animationTarget = Offset(playerPos.x.toFloat(), playerPos.z.toFloat()).toLocalTileBlock() mapControl = MapControl.Target diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt index 038dd28..666a0bb 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WaypointsView.kt @@ -211,8 +211,13 @@ fun WaypointsView(waypoints: List, onRefresh: () -> Unit, onClick }.onPointerEvent(PointerEventType.Exit) { onHover(wp, false) }.animateItemPlacement()) { - editedWaypoint = wp - addEditWaypoint = true + if (editedWaypoint == wp) { + editedWaypoint = null + addEditWaypoint = false + } else { + editedWaypoint = wp + addEditWaypoint = true + } } } item { diff --git a/src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt b/src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt index edcfa05..e4f1194 100644 --- a/src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt +++ b/src/main/java/dev/wefhy/whymap/compose/ui/WorkaroundTextFieldSimple.kt @@ -8,20 +8,25 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.TextFieldColors +import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults.indicatorLine import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.input.key.* +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import dev.wefhy.whymap.compose.utils.WorkaroundInteractions import dev.wefhy.whymap.compose.utils.WorkaroundKeyEventRecognizer -import java.awt.event.KeyEvent.VK_BACK_SPACE + @Composable fun WorkaroundTextFieldSimple( @@ -44,47 +49,79 @@ fun WorkaroundTextFieldSimple( minLines: Int = 1, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = TextFieldDefaults.TextFieldShape, - colors: TextFieldColors = TextFieldDefaults.textFieldColors() + colors: TextFieldColors = TextFieldDefaults.textFieldColors(), + onSubmit: () -> Unit = {}, + onTab: () -> Unit = {}, ) { // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) - var textFieldValue by remember { mutableStateOf("") } - remember(value) { textFieldValue = value } + var text by remember { mutableStateOf(TextFieldValue(value)) } + remember(value) { text = text.copy(text = value) } val recognizer = remember { - WorkaroundKeyEventRecognizer(object : WorkaroundInteractions { - override fun onCharacterTyped(c: Char) { - onValueChange(textFieldValue + c) + WorkaroundKeyEventRecognizer(object : WorkaroundInteractions() { + + override fun onCharacterTyped(c: Char, shift: Boolean) { + val char = if (shift) c.uppercaseChar() else c + val newText = text.text.take(text.selection.min) + char + text.text.drop(text.selection.max) + text = text.copy( + text = newText, + selection = textRange(text.selection.min + 1, text.selection.max + 1) + ) + onValueChange(newText) } override fun onBackspace() { - onValueChange(textFieldValue.dropLast(1)) + val newText = if (text.selection.min == text.selection.max) { + text.text.safeTake(text.selection.min - 1) + text.text.drop(text.selection.max) + } else { + text.text.take(text.selection.min) + text.text.drop(text.selection.max) + } +// val newText = text.text.dropLast(1) + text = text.copy( + text = newText, + selection = cursor(text.selection.min - 1) + ) + onValueChange(newText) } override fun onDelete() { - onValueChange(textFieldValue.drop(1)) + val newText = text.text.drop(1) + text = text.copy( + text = newText, + selection = cursor(text.selection.min - 1) + ) + onValueChange(newText) } override fun onEnter() { - //submit + onSubmit() } - override fun onLeftArrow() { - + override fun onTab() { + onTab() } - override fun onRightArrow() { + override fun onLeftArrow(shift: Boolean) { + text = text.copy( + selection = cursor(text.selection.min - 1) + ) + } + override fun onRightArrow(shift: Boolean) { + text = text.copy( + selection = cursor(text.selection.max + 1) + ) } }) } @OptIn(ExperimentalMaterialApi::class) (BasicTextField( - value = value, + value = text, modifier = modifier .background(colors.backgroundColor(enabled).value, shape) .indicatorLine(enabled, isError, interactionSource, colors) @@ -94,9 +131,9 @@ fun WorkaroundTextFieldSimple( ) .onKeyEvent { recognizer.onKeyEvent(it) - false + true }, - onValueChange = {},//onValueChange, + onValueChange = { text = it }, enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, diff --git a/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt index e914104..f520bac 100644 --- a/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt +++ b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundInteractions.kt @@ -2,11 +2,18 @@ package dev.wefhy.whymap.compose.utils -interface WorkaroundInteractions { - fun onCharacterTyped(c: Char) - fun onBackspace() - fun onDelete() - fun onEnter() - fun onLeftArrow() - fun onRightArrow() +import androidx.compose.ui.text.TextRange + +abstract class WorkaroundInteractions { + abstract fun onCharacterTyped(c: Char, shift: Boolean = false) + abstract fun onBackspace() + abstract fun onDelete() + abstract fun onEnter() + abstract fun onLeftArrow(shift: Boolean = false) + abstract fun onRightArrow(shift: Boolean = false) + abstract fun onTab() + protected inline fun cursor(position: Int) = textRange(position, position) + protected inline fun textRange(min: Int, max: Int) = TextRange(min.coerceAtLeast(0), max.coerceAtLeast(0)) + protected fun String.replace(range: TextRange, replacement: String) = take(range.min) + replacement + drop(range.max) + protected fun String.safeTake(index: Int) = take(index.coerceAtLeast(0)) } \ No newline at end of file diff --git a/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt index a26ce26..ba3078b 100644 --- a/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt +++ b/src/main/java/dev/wefhy/whymap/compose/utils/WorkaroundKeyEventRecognizer.kt @@ -4,10 +4,25 @@ package dev.wefhy.whymap.compose.utils import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.Key.Companion +import java.awt.event.KeyEvent.VK_BACK_SPACE import java.awt.event.KeyEvent as AwtKeyEvent class WorkaroundKeyEventRecognizer(val interactions: WorkaroundInteractions) { + companion object { + + } + + object Weird { + val lShift = 0x154 + val rShift = 0x158 + val leftArraw = 263 + val rightArraw = 262 + val tab = 258 + val enter = 257 + val backspace = 259 + } + val letters = setOf( Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, Key.H, Key.I, Key.J, Key.K, Key.L, Key.M, Key.N, Key.O, Key.P, Key.Q, Key.R, Key.S, Key.T, Key.U, Key.V, Key.W, Key.X, Key.Y, Key.Z ) @@ -34,13 +49,22 @@ class WorkaroundKeyEventRecognizer(val interactions: WorkaroundInteractions) { Key.Backspace -> interactions.onBackspace() Key.Delete -> interactions.onDelete() Key.Enter -> interactions.onEnter() - Key.DirectionLeft -> interactions.onLeftArrow() - Key.DirectionRight -> interactions.onRightArrow() - in letters -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) - in numbers -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) - in numpad -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) - in symbols -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first()) + Key.DirectionLeft -> interactions.onLeftArrow(keyEvent.isShiftPressed) + Key.DirectionRight -> interactions.onRightArrow(keyEvent.isShiftPressed) + Key.Spacebar -> interactions.onCharacterTyped(' ', keyEvent.isShiftPressed) + in letters -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first(), keyEvent.isShiftPressed) + in numbers -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first(), keyEvent.isShiftPressed) + in numpad -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first(), keyEvent.isShiftPressed) + in symbols -> interactions.onCharacterTyped(AwtKeyEvent.getKeyText(key.nativeKeyCode).first(), keyEvent.isShiftPressed) else -> { + when(key.nativeKeyCode) { + Weird.tab -> interactions.onTab() + Weird.backspace -> interactions.onBackspace() + Weird.enter -> interactions.onEnter() + Weird.leftArraw -> interactions.onLeftArrow(keyEvent.isShiftPressed) + Weird.rightArraw -> interactions.onRightArrow(keyEvent.isShiftPressed) + else -> println("Unhandled key: $key, native: ${key.nativeKeyCode}") + } } } } diff --git a/src/main/java/dev/wefhy/whymap/tiles/region/MapRegionManager.kt b/src/main/java/dev/wefhy/whymap/tiles/region/MapRegionManager.kt index 72325d3..7b3641a 100644 --- a/src/main/java/dev/wefhy/whymap/tiles/region/MapRegionManager.kt +++ b/src/main/java/dev/wefhy/whymap/tiles/region/MapRegionManager.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import net.minecraft.client.MinecraftClient import java.util.concurrent.ConcurrentHashMap context(CurrentWorldProvider) @@ -57,7 +58,7 @@ class MapRegionManager { private suspend fun cleanupRegions() = withContext(WhyDispatchers.LowPriority) { // TODO make sure this runs on correct dispatcher to avoid context switching - val playerPos = (currentWorld as? CurrentWorld)?.player?.pos + val playerPos = MinecraftClient.getInstance()?.player?.pos regionLoaders.values.map { launch { it.clean(playerPos) } }.forEach { it.join() } }