KeyEvents.kt
src/jvmMain/kotlin/afield/graphics/vulkan/util/KeyEvents.kt · 164 lines
package afield.graphics.vulkan.util
Keyboard handlers for a Vulkan window, polled with the renderer.
KeyEvents registers handlers per key; a down handler's DownResult says whether it stays registered, runs a callback on the matching release, or starts a per-frame generator that runs while the key is held.
import afield.graphics.vulkan.Renderer
import afield.document.context.fromContext
import afield.app.diagnostics.logging.lprintln
import org.lwjgl.glfw.GLFW
import afield.runtime.lifecycle.Generator
import afield.runtime.lifecycle.GeneratorScope
sealed class DownResult
Result contract for "down" handlers.
sealed class DownResult {
data object Keep : DownResult()
Keep the handler registered (default behavior).
data object Remove : DownResult()
Unregister the handler after this down event.
data class OnUp(val onUp: () -> Unit) : DownResult()
Register a one-shot callback that will be invoked the next time the same key/button goes UP (i.e., the matching release for this specific press). The handler itself remains registered.
data class WhileDown(val generator: Generator<Boolean, Boolean>) : DownResult()
Register a per-frame generator that will be stepped every poll.
The Boolean input is true while the key is down, false after release.
The generator is driven until completion.
constructor(g : suspend GeneratorScope<Boolean, Boolean>.() -> Unit) : this(Generator(g))
class KeyEvents(val renderer: Renderer = fromContext(), val name: String = "__keyevents__")
companion object
val namesToGLFW = mutableMapOf<String, Int>()
val GLTFToNames = mutableMapOf<Int, String>()
init
init {
GLFW::class.java.declaredFields.filter { it.type == Integer.TYPE && it.name.lowercase().contains("_key_") }
.associate { it.name.lowercase().replace("glfw_key_", "") to it.get(null) as Int }.forEach { (k, v) ->
namesToGLFW[k] = v
GLTFToNames[v] = k
}
}
}
Existing API kept, but underlying type is now () -> DownResult
private val down = mutableMapOf<String, MutableMap<String, () -> DownResult>>()
private val up = mutableMapOf<String, MutableMap<String, () -> Boolean>>()
Per-press, transient onUp callbacks created by down-handlers
private val pendingUp = mutableMapOf<String, MutableList<() -> Unit>>()
Per-press, transient per-frame generators created by down-handlers
private val pendingGenerators = mutableMapOf<String, MutableList<Generator<Boolean, Boolean>>>()
fun onDownAndUp(key: String, name: String, callback: () -> DownResult) {
fun onDownAndUp(key: String, name: String, callback: () -> DownResult) {
down.computeIfAbsent(key) { mutableMapOf() }[name] = callback
}
fun onDownUntilUp(key: String, name: String, onUp: () -> Unit) {
Registration (backwards compatible)
fun onDownUntilUp(key: String, name: String, onUp: () -> Unit) {
onDownAndUp(key, name) { DownResult.OnUp(onUp) }
}
fun onDown(key: String, name: String, callback: () -> Boolean) {
fun onDown(key: String, name: String, callback: () -> Boolean) {
onDownAndUp(key, name) { if (callback()) DownResult.Keep else DownResult.Remove }
}
fun onUp(key: String, name: String, callback: () -> Boolean) {
fun onUp(key: String, name: String, callback: () -> Boolean) {
up.computeIfAbsent(key) { mutableMapOf() }[name] = callback
}
private val wasDown = mutableSetOf<String>()
init
body — 81 lines, from line 83
init {
renderer.window.onPoll[name] = sequence {
while (true) {
val keys: Set<String> = down.keys + up.keys
val nowDown = mutableSetOf<String>()
keys.forEach { n ->
namesToGLFW[n]?.let { if (renderer.window.isKeyPressed(it)) nowDown.add(n) }
?: lprintln("warning,input", "no such key: $n")
}
val newDown = nowDown - wasDown
val newUp = wasDown - nowDown
// Fire DOWN; collect UntilUp callbacks; decide handler retention
newDown.forEach { n ->
down[n]?.let { map ->
val toKeep = mutableListOf<Pair<String, () -> DownResult>>()
map.forEach { (k, cb) ->
when (val r = cb()) {
DownResult.Keep -> toKeep += k to cb
DownResult.Remove -> { /* drop */
}
is DownResult.OnUp -> {
pendingUp.getOrPut(n) { mutableListOf() }.add(r.onUp)
toKeep += k to cb // keep handler for future presses
}
is DownResult.WhileDown -> {
val gen = r.generator
val first = gen.start()
if (!gen.isCompleted) {
pendingGenerators.getOrPut(n) { mutableListOf() }.add(gen)
}
// ignore first output
toKeep += k to cb // keep handler for future presses
}
}
}
// replace with only those retained
map.clear()
toKeep.forEach { (k, cb) -> map[k] = cb }
}
}
// Step per-frame generators for keys that have generators (down or released)
pendingGenerators.entries.toList().forEach { (key, gens) ->
val input = nowDown.contains(key)
val toRemove = mutableListOf<Generator<Boolean, Boolean>>()
gens.forEach { g ->
if (!g.isCompleted) {
g.next(input)
}
if (g.isCompleted) toRemove += g
}
gens.removeAll(toRemove)
if (gens.isEmpty()) pendingGenerators.remove(key)
}
// Fire transient onUp from down-handlers first
newUp.forEach { n ->
pendingGenerators.remove(n) // remaining gens will be driven above with input=false
pendingUp.remove(n)?.forEach { it.invoke() }
}
// Then fire normal UP handlers; allow self-unregister via Boolean
newUp.forEach { n ->
up[n]?.let { map ->
map.values.retainAll { it.invoke() }
}
}
wasDown.clear()
wasDown.addAll(nowDown)
yield(true)
}
}
}
}