> For the complete documentation index, see [llms.txt](https://docs.nursultan.fun/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nursultan.fun/examples/keystrokes.md).

# Keystrokes

Shows which movement keys are held, plus mouse CPS. A good example of how a HUD comes together: state is collected in an input handler, and the frame only draws.

```kotlin
name("Keystrokes")
description("Shows the movement keys you are holding")

key(Key.K)

val x = slider("X", 10, 0, 400)
val y = slider("Y", 100, 0, 400)
val size = slider("Size", 22, 12, 40)
val accent = colorPicker("Pressed colour", Colors.rgba(90, 200, 255, 255))
val showCps = checkBox("Show CPS", true)

var forward = false
var backward = false
var left = false
var right = false
var jump = false

val clicks = ArrayDeque<Long>()

onEnable {
    clicks.clear()
}

on<MoveInputEvent> { e ->
    forward = e.forward()
    backward = e.backward()
    left = e.left()
    right = e.right()
    jump = e.jump()
}

on<KeyEvent> { e ->
    if (e.pressed() && e.matches(Key.MOUSE_1)) {
        clicks.addLast(client.millis())
    }
}

fun cps(): Int {
    val now = client.millis()
    while (clicks.isNotEmpty() && now - clicks.first() > 1000L) {
        clicks.removeFirst()
    }
    return clicks.size
}

on<Render2DEvent> { e ->
    whenInGame {
        val r = e.render()
        val cell = size.value()
        val gap = 2f
        val originX = x.value()
        val originY = y.value()

        fun keyBox(column: Int, row: Int, label: String, pressed: Boolean) {
            val boxX = originX + column * (cell + gap)
            val boxY = originY + row * (cell + gap)
            val background = if (pressed) accent.value() else Colors.rgba(0, 0, 0, 130)
            val text = if (pressed) Colors.BLACK else Colors.WHITE
            r.roundedRect(boxX, boxY, cell, cell, 4f, background)
            val width = r.textWidth(label, cell * 0.45f)
            r.text(label, boxX + (cell - width) / 2f, boxY + cell * 0.3f, cell * 0.45f, text)
        }

        keyBox(1, 0, "W", forward)
        keyBox(0, 1, "A", left)
        keyBox(1, 1, "S", backward)
        keyBox(2, 1, "D", right)

        val spaceY = originY + 2 * (cell + gap)
        val spaceWidth = cell * 3 + gap * 2
        r.roundedRect(originX, spaceY, spaceWidth, cell * 0.6f, 4f,
            if (jump) accent.value() else Colors.rgba(0, 0, 0, 130))

        if (showCps.value()) {
            val text = cps().toString() + " cps"
            r.text(text, originX, spaceY + cell * 0.8f, cell * 0.4f, Colors.WHITE)
        }
    }
}
```

## Things worth noticing

**State is collected outside the frame.** `MoveInputEvent` arrives once per tick while frames come dozens of times a second. Reading input inside `Render2DEvent` would not work — it is simply not there.

**CPS is a queue of timestamps.** Push the time of each click, drop everything older than a second, and what is left is clicks per second.

**Settings are read in the frame, not cached.** The player can drag a slider at any moment, and the HUD should follow immediately.

**A local function instead of copy-paste.** `keyBox` is declared inside the handler, so it sees `r` and the sizes without dragging them through arguments.
