net.peanuuutz.fork.ui.internal.GlobalUIContext.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fork-ui Show documentation
Show all versions of fork-ui Show documentation
Comprehensive API designed for Minecraft modders
The newest version!
package net.peanuuutz.fork.ui.internal
import androidx.compose.runtime.BroadcastFrameClock
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.snapshots.ObserverHandle
import androidx.compose.runtime.snapshots.Snapshot
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
import net.minecraft.client.MinecraftClient
import net.peanuuutz.fork.coroutine.core.BroadcastTickClock
import net.peanuuutz.fork.coroutine.core.Client
import net.peanuuutz.fork.coroutine.core.ForkClientMainScope
import net.peanuuutz.fork.coroutine.core.ForkGlobalScope
import net.peanuuutz.fork.coroutine.core.delayTicks
import net.peanuuutz.fork.util.common.nanoNow
import net.peanuuutz.fork.util.fork.ForkLogger
internal object GlobalUIContext : ClientLifecycleEvents.ClientStopping {
// -------- State --------
private var isRunning: Boolean = false
// -------- Frame Clock --------
private var hasScheduledFrame: Boolean = false
private val frameClock: BroadcastFrameClock = BroadcastFrameClock { hasScheduledFrame = true }
private var frameClockJob: Job? = null
// -------- Tick Clock --------
private var hasScheduledTick: Boolean = false
private val tickClock: BroadcastTickClock = BroadcastTickClock { hasScheduledTick = true }
private var tickClockJob: Job? = null
// -------- Recomposer --------
val recomposer: Recomposer = Recomposer(Dispatchers.Client + tickClock)
private var recomposerJob: Job? = null
// -------- Snapshot Manager --------
private val applyFlow: MutableSharedFlow = MutableSharedFlow(
extraBufferCapacity = 16,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
private var applyJob: Job? = null
private var applyObserver: ObserverHandle? = null
// -------- Setup --------
fun ensureStarted() {
if (isRunning) {
return
}
isRunning = true
setupSnapshotManager()
setupRecomposer()
setupFrameClock()
setupTickClock()
}
private fun setupSnapshotManager() {
applyJob = ForkClientMainScope.launch(CoroutineName("GlobalWriter")) {
applyFlow.collect {
// We apply changes in another scope to avoid blocking
Snapshot.sendApplyNotifications()
}
}
applyObserver = Snapshot.registerGlobalWriteObserver {
applyFlow.tryEmit(Unit)
}
}
private fun setupRecomposer() {
recomposerJob = ForkGlobalScope.launch(frameClock + CoroutineName("Recomposer")) {
recomposer.runRecomposeAndApplyChanges()
}
}
private fun setupFrameClock() {
ForkGlobalScope.launch(CoroutineName("Frame")) {
while (isRunning) {
if (hasScheduledFrame) {
hasScheduledFrame = false
// We're on another thread, so switch to main thread to avoid concurrent issues
ForkClientMainScope.launch {
try {
frameClock.sendFrame(nanoNow())
} catch (e: Exception) {
val message = "Exception occurred while sending frame"
ForkLogger.error(message, e)
}
}
}
delay(10L)
}
}
}
private fun setupTickClock() {
// Ticking is available on Dispatchers.Client but not Dispatchers.Default
ForkClientMainScope.launch(CoroutineName("Tick")) {
var ticks = 0L
while (isRunning) {
if (hasScheduledTick) {
hasScheduledTick = false
// We're already on main thread
try {
tickClock.sendTick(ticks)
} catch (e: Exception) {
val message = "Exception occurred while sending tick"
ForkLogger.error(message, e)
}
}
delayTicks(1L)
ticks++
}
}
}
// -------- Dispose --------
override fun onClientStopping(client: MinecraftClient) {
isRunning = false
disposeSnapshotManager()
disposeRecomposer()
disposeFrameClock()
disposeTickClock()
}
private fun disposeSnapshotManager() {
applyJob?.cancel()
applyJob = null
applyObserver?.dispose()
applyObserver = null
}
private fun disposeRecomposer() {
recomposer.close()
recomposerJob?.cancel()
recomposerJob = null
}
private fun disposeFrameClock() {
frameClock.cancel()
frameClockJob?.cancel()
frameClockJob = null
}
private fun disposeTickClock() {
tickClock.cancel()
tickClockJob?.cancel()
tickClockJob = null
}
}