MouseEvents.kt
src/jvmMain/kotlin/afield/graphics/vulkan/util/MouseEvents.kt · 266 lines
package afield.graphics.vulkan.util
Mouse input from a Vulkan renderer's window, delivered to named callbacks.
MouseEvents registers handlers for down, up, move, drag and scroll, keyed by a name so that re-running a box replaces its handler rather than adding another. Each handler receives a MouseEvent with position, delta, scroll, buttons held, and the window size.
import afield.runtime.lifecycle.Generator
import afield.graphics.vulkan.Renderer
import afield.document.context.fromContext
import afield.app.diagnostics.logging.lprintln
import afield.math.linalg.Vec2
import org.lwjgl.glfw.GLFW
data class MouseEvent
data class MouseEvent(
/** Where the pointer is, in points from the window's top-left corner — the same units as [windowSize], not framebuffer pixels. */
val pos: Vec2,
/** How far the pointer moved since the previous poll, in the same points. Only [Type.MOVE] and [Type.DRAG] carry one; a press, release or scroll arrives with zero. */
val delta: Vec2 = Vec2(),
/** Wheel and trackpad movement since the previous poll, horizontal then vertical. Only a [Type.SCROLL] event carries one. */
val scroll: Vec2 = Vec2(),
/**
* The buttons held at this poll, under the names `left`, `right`, `middle` and the GLFW numbers.
* Only buttons some handler asked about are read, so one with no `onDown`, `onUp` or per-button
* `onDrag` never appears here however hard it is held. An `onDrag` with no button named asks for
* left, right and middle.
*/
val buttonsDown: Set<String> = emptySet(),
/** Which button this event is about. A press, a release and a per-button drag name one; a move, a scroll and an any-button drag leave it null. */
val button: String? = null,
/** Which of the five kinds of event this is. */
val type: Type,
/** The window's size, in the same units as [pos]: window points, not framebuffer pixels. */
val windowSize: Vec2 = Vec2(1f, 1f),
) {
enum class Type { MOVE, DOWN, UP, DRAG, SCROLL }
fun isDown(name: String) = buttonsDown.contains(name)
val uv: Vec2 get() = Vec2(pos.x / windowSize.x, pos.y / windowSize.y)
pos as a fraction of the window: (0, 0) at its top-left corner, (1, 1) at its bottom-right.
class MouseEvents
class MouseEvents(
/** The renderer whose window is polled. This becomes that window's `mouseEvents` if it does not already have one. */
val renderer: Renderer = fromContext(),
/**
* The key this polling takes in the window's poll map, which is not the name a handler is
* registered under. A second [MouseEvents] built with the same name takes over the polling from
* the first, so re-running a box leaves one; the `name` passed to `onDown`, `onMove` and the
* rest is what keeps a box from stacking up duplicate handlers inside it.
*/
val name: String = "__mouseevents__"
) {
companion object
companion object {
What onDrag(name) without a button listens to.
private val ANY_DRAG_BUTTONS = setOf("left", "right", "middle")
val namesToGLFW = mutableMapOf<String, Int>()
val GLFWToNames = mutableMapOf<Int, String>()
init
init {
GLFW::class.java.declaredFields
.filter { it.type == Integer.TYPE && it.name.lowercase().contains("_mouse_button_") }
.associate { it.name.lowercase().replace("glfw_mouse_button_", "") to it.get(null) as Int }
.forEach { (k, v) ->
namesToGLFW[k] = v
GLFWToNames[v] = k
}
namesToGLFW["left"] = GLFW.GLFW_MOUSE_BUTTON_LEFT
namesToGLFW["right"] = GLFW.GLFW_MOUSE_BUTTON_RIGHT
namesToGLFW["middle"] = GLFW.GLFW_MOUSE_BUTTON_MIDDLE
GLFWToNames[GLFW.GLFW_MOUSE_BUTTON_LEFT] = "left"
GLFWToNames[GLFW.GLFW_MOUSE_BUTTON_RIGHT] = "right"
GLFWToNames[GLFW.GLFW_MOUSE_BUTTON_MIDDLE] = "middle"
for (b in GLFW.GLFW_MOUSE_BUTTON_1..GLFW.GLFW_MOUSE_BUTTON_LAST) {
val n = GLFWToNames[b] ?: b.toString()
namesToGLFW.putIfAbsent(n, b)
GLFWToNames.putIfAbsent(b, n)
}
}
}
Down now returns DownResult; keep Boolean overload for compatibility
private val down = mutableMapOf<String, MutableMap<String, (MouseEvent) -> DownResult>>()
private val up = mutableMapOf<String, MutableMap<String, (MouseEvent) -> Boolean>>()
private val move = mutableMapOf<String, (MouseEvent) -> Boolean>()
private val drag = mutableMapOf<String, (MouseEvent) -> Boolean>()
private val buttonDrag = mutableMapOf<String, MutableMap<String, (MouseEvent) -> Boolean>>()
private val scroll = mutableMapOf<String, (MouseEvent) -> Boolean>()
Transient per-press onUp callbacks created by down-handlers
private val pendingUp = mutableMapOf<String, MutableList<(MouseEvent) -> Unit>>()
Transient per-press per-frame generators created by down-handlers
private val pendingGenerators = mutableMapOf<String, MutableList<Generator<Boolean, Boolean>>>()
fun onDown(button: String, name: String, callback: () -> Boolean) =
Registration (backwards compatible)
fun onDown(button: String, name: String, callback: () -> Boolean) =
onDown(button, name) { _ -> if (callback()) DownResult.Keep else DownResult.Remove }
fun onDown(button: String, name: String, callback: (MouseEvent) -> DownResult) {
fun onDown(button: String, name: String, callback: (MouseEvent) -> DownResult) {
down.computeIfAbsent(button) { mutableMapOf() }[name] = callback
}
fun onDownUntilUp(button: String, name: String, onUp: (MouseEvent) -> Unit) =
Handy helper to attach a one-shot onUp for this button press.
fun onDownUntilUp(button: String, name: String, onUp: (MouseEvent) -> Unit) =
onDown(button, name) { ev -> DownResult.OnUp { onUp(ev) } }
fun onUp(button: String, name: String, callback: () -> Boolean) =
fun onUp(button: String, name: String, callback: () -> Boolean) =
onUp(button, name) { _ -> callback() }
fun onUp(button: String, name: String, callback: (MouseEvent) -> Boolean) {
fun onUp(button: String, name: String, callback: (MouseEvent) -> Boolean) {
up.computeIfAbsent(button) { mutableMapOf() }[name] = callback
}
fun onMove(name: String, callback: (MouseEvent) -> Boolean) { move[name] = callback }
fun onDrag(name: String, callback: (MouseEvent) -> Boolean) { drag[name] = callback }
Calls callback whenever the mouse moves with any of the left, right or middle buttons down.
Return false to remove it. Needs no onDown beside it.
fun onDrag(button: String, name: String, callback: (MouseEvent) -> Boolean) {
Calls callback whenever the mouse moves with button down. Return false to remove it.
fun onDrag(button: String, name: String, callback: (MouseEvent) -> Boolean) {
buttonDrag.computeIfAbsent(button) { mutableMapOf() }[name] = callback
}
fun onScroll(name: String, callback: (MouseEvent) -> Boolean) { scroll[name] = callback }
fun remove(name: String)
Remove only the callbacks installed under this name, leaving other tools' input intact.
fun remove(name: String) {
move.remove(name)
drag.remove(name)
scroll.remove(name)
down.values.forEach { it.remove(name) }
up.values.forEach { it.remove(name) }
buttonDrag.values.forEach { it.remove(name) }
}
private val wasDown = mutableSetOf<String>()
private var lastPos = Vec2()
init
body — 123 lines, from line 143
init {
lastPos.set(renderer.window.cursor)
if (renderer.window.mouseEvents==null) renderer.window.mouseEvents = this
renderer.window.onPoll[name] = sequence {
while (true) {
val pos = Vec2(renderer.window.cursor)
val delta = Vec2(pos).sub(lastPos)
lastPos.set(pos)
val scrollDelta = renderer.window.consumeScrollDelta()
// Only the buttons some handler names are polled. A drag handler counts: it used to
// fire only when an onDown or onUp for the same button happened to be registered.
val buttonsRequested: Set<String> =
(down.keys + up.keys + buttonDrag.keys + (if (drag.isNotEmpty()) ANY_DRAG_BUTTONS else emptySet())).toSet()
// The cursor is in window points; Window.width/height are the framebuffer's
// pixels, twice as many on a Retina display, which put uv's far corner at (0.5, 0.5).
val points = renderer.window.size()
val size = Vec2(points.w.toFloat(), points.h.toFloat())
val nowDown = mutableSetOf<String>()
buttonsRequested.forEach { bName ->
val glfw = namesToGLFW[bName]
if (glfw != null) {
if (renderer.window.isMouseButtonPressed(glfw)) nowDown.add(bName)
} else lprintln("warning,input", "no such mouse button: $bName")
}
val newDown = nowDown - wasDown
val newUp = wasDown - nowDown
val buttonsDownSnapshot = nowDown.toSet()
if (delta.x != 0f || delta.y != 0f) {
if (move.isNotEmpty()) {
val ev = MouseEvent(pos, delta, Vec2(), buttonsDownSnapshot, null, MouseEvent.Type.MOVE, size)
move.values.retainAll { it(ev) }
}
}
if ((delta.x != 0f || delta.y != 0f) && nowDown.isNotEmpty()) {
if (drag.isNotEmpty()) {
val ev = MouseEvent(pos, delta, Vec2(), buttonsDownSnapshot, null, MouseEvent.Type.DRAG, size)
drag.values.retainAll { it(ev) }
}
nowDown.forEach { bName ->
buttonDrag[bName]?.let { map ->
val ev = MouseEvent(pos, delta, Vec2(), buttonsDownSnapshot, bName, MouseEvent.Type.DRAG, size)
map.values.retainAll { it(ev) }
}
}
}
// DOWN: fire; collect UntilUp; decide handler retention
newDown.forEach { bName ->
down[bName]?.let { map ->
val toKeep = mutableListOf<Pair<String, (MouseEvent) -> DownResult>>()
val ev = MouseEvent(pos, Vec2(), Vec2(), buttonsDownSnapshot, bName, MouseEvent.Type.DOWN, size)
map.forEach { (k, cb) ->
when (val r = cb(ev)) {
DownResult.Keep -> toKeep += k to cb
DownResult.Remove -> { /* drop */ }
is DownResult.OnUp -> {
pendingUp.getOrPut(bName) { mutableListOf() }.add { upEv ->
r.onUp.invoke()
}
toKeep += k to cb
}
is DownResult.WhileDown -> {
val gen = r.generator
val first = gen.start()
if (!gen.isCompleted) {
pendingGenerators.getOrPut(bName) { mutableListOf() }.add(gen)
}
// ignore first output
toKeep += k to cb
}
}
}
map.clear()
toKeep.forEach { (k, cb) -> map[k] = cb }
}
}
// Step per-frame generators for buttons that have generators (down or released)
pendingGenerators.entries.toList().forEach { (bName, gens) ->
val input = nowDown.contains(bName)
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(bName)
}
// UP: first fire transient per-press callbacks, then regular UP handlers
if (newUp.isNotEmpty()) {
newUp.forEach { bName ->
val ev = MouseEvent(pos, Vec2(), Vec2(), buttonsDownSnapshot, bName, MouseEvent.Type.UP, size)
pendingGenerators.remove(bName) // remaining gens driven above with input=false
pendingUp.remove(bName)?.forEach { it(ev) }
up[bName]?.let { map ->
map.values.retainAll { it(ev) }
}
}
}
if (scrollDelta.x != 0f || scrollDelta.y != 0f) {
if (scroll.isNotEmpty()) {
val ev = MouseEvent(pos, Vec2(), scrollDelta, buttonsDownSnapshot, null, MouseEvent.Type.SCROLL, size)
scroll.values.retainAll { it(ev) }
}
}
wasDown.clear()
wasDown.addAll(nowDown)
yield(true)
}
}
}
}