Input
OkInput reads the keyboard and mouse each frame and exposes both an edge/level query API and a digested OkInputState. You usually reach it through OkCore::getInput() inside the step callback. Keys are referenced through the platform-independent OkKey enum (see input/keys.hpp).
OkInput methods
| Method | Purpose |
|---|---|
bool isKeyJustPressed(OkKey key) const |
True only on the frame the key goes down. |
bool isKeyHeld(OkKey key) const |
True while the key is held. |
bool isKeyJustReleased(OkKey key) const |
True only on the frame the key is released. |
OkInputState getState() const |
The digested per-frame input state. |
void injectKey(OkKey key, double durationSeconds) |
Synthesize a key press (used to drive the app programmatically). |
void injectPointerTo(double x, double y) |
Put the injected pointer at a place, in window pixels from the top left. |
void injectPointerBy(double dx, double dy) |
Move it from where it is, in the same pixels. |
void injectPointerButton(int button, bool down) |
Hold or release one of its buttons: 0 left, 1 right, 2 middle. |
void injectPointerWheel(double notches) |
Turn its wheel, positive away from the hand. |
void injectedPointer(double *x, double *y) const |
Where it is. |
bool injectedButton(int button) const |
Whether that button is held. |
double takeInjectedWheel() |
What the wheel has turned since this was last asked, and clears it. |
bool injectedPointerUsed() const |
Whether anything has driven it at all. |
void setPhysicalInputEnabled(bool enabled) |
Enable/disable physical keyboard/mouse input. |
void setCursorCaptured(bool captured) |
Capture (hide + lock for mouse-look) or release the OS cursor. |
void setPointerLockOnClick(bool enabled) |
Whether a click may take the pointer at all (default on). |
bool isCursorCaptured() const |
Whether the cursor is currently captured. |
void setTextCapture(bool captured) |
Text capture (the console): the normal accessors and getState() report nothing while set, so typing cannot trigger gameplay keys. |
bool isKeyJustPressedRaw/isKeyHeldRaw(OkKey) |
Variants that ignore the text-capture flag (console internals). |
void onChar(unsigned int codepoint) |
Queue a typed character (fed by the GLFW char callback; printable injected keys also land here while captured). |
std::string drainChars() |
Return and clear the characters typed since the last call. |
Mouse cursor (pointer lock)
Mouse-look uses a pointer-lock model rather than holding the cursor captured for the whole session:
- The app starts with a normal OS cursor, so the window can be moved (title bar), sent to another desktop, and the OS chrome used normally.
- Clicking inside the render area captures the cursor (hidden + locked) and enables mouse-look. Clicks on the title bar / OS chrome are not delivered to the engine, so they keep working.
- ESC releases the cursor (browser style). When the cursor is already released, ESC sets
exitinstead (the app's quit request). - Losing window focus releases the cursor automatically (frees the OS pointer and, on macOS, restores system-wide mouse acceleration); the user clicks back into the view to resume mouse-look.
- With physical input disabled (
setPhysicalInputEnabled(false), e.g.--no-input) the cursor is never captured; drive the view through the MCPviewtool instead.
Blocking input for a while
blockPhysicalInput(seconds) is the same gate with a deadline on it, for
an agent that needs the view to hold still while it measures something.
It exists in that shape because of what goes wrong: a block is invisible
-- the keyboard just stops answering -- so one that is never lifted looks
from a chair exactly like a hang.
So a block expires by itself, the combo in RELEASE_MODS +
RELEASE_KEY lifts it, and the engine draws a line saying input is held
and how to take it back. Nothing running in the background can talk its
way past those. physicalInputBlockedFor() reports what is left: 0
when input is free, and a negative number for a block with no deadline
(what the launch flag asks for).
The injected pointer
Keys are half of driving an application; the other half is the pointer,
and an application with an interface of its own is nearly all pointer.
The engine keeps an injected pointer -- a position in window pixels
from the top left, three buttons and a wheel -- and says whether
anything has used it. What to do with it is the application's business,
exactly as with injected keys: nothing here pretends to be the window
system, and a UI library reading the window system directly will not
see it unless the application hands it over. The MCP mouse tool drives
these, and the MCP reference shows the
handover for Dear ImGui.
takeInjectedWheel() clears what it returns. Read it once a frame: a
notch delivered twice is one turn of the wheel doing two steps.
injectedPointerUsed() is a question about now, not a latch: it
goes false two seconds after the last call that touched the pointer,
and the moment the physical mouse moves. A hand on the desk beats
anything driving from outside, and an application that trusted a latch
here leaves the person at the window with a mouse that does nothing.
An application gate is not somebody being held out
setPhysicalInputEnabled(false) and blockPhysicalInput(seconds) reach
the same gate, and they are not the same thing. The first is an
application routing input somewhere else for a moment -- an editor does
it every time the cursor crosses one of its panels, so that a drag
inside a window does not also fly the camera. The second is somebody
outside holding the keyboard.
Only the second is reported by physicalInputBlockedFor() and only the
second draws the notice on screen. They were one thing once, and the
result was an editor announcing that an agent held the keyboard every
time the mouse touched a window.
Combos: at once, and one after another
A combo comes in the two kinds okinawa.js named: simultaneous,
several keys held together, and consecutive, one key after another
inside a time window. Only the simultaneous one is implemented; the
press history below is what the other would be matched on.
isComboJustPressed(mods, key) is the simultaneous one: the
modifiers already held, the key going down this frame, and exactly
those modifiers -- a combo that fired on "at least" would swallow every
gesture that contains it.
It reads the DEVICE and not what the application is allowed to see, so a combo still works while input is blocked. That is the case the engine itself needs: the gesture that gives the keyboard back is wanted precisely when the keyboard is being ignored.
RELEASE_MODS, RELEASE_KEY and releaseComboName() hold that gesture
in one place, so the line the engine draws and the combo that actually
works cannot drift apart. It is not escape, which is what it used
first: macOS does not deliver Escape to an application while Control
is held -- measured on the device, with the modifiers arriving, the
Escape never arriving, and ctrl+shift+E arriving in the same breath.
consumeKeyUntilReleased(key) is what makes a combo usable rather
than merely detectable. The keys of a combo mean something on their
own, so whoever acts on one has to take them out of the frame and keep
them out until the gesture is over. Without it the release worked
exactly once: it lifted the block, and the escape still held closed the
application a frame later. A letter is no safer by itself --
ctrl+shift+E walks the avatar upwards, because E is a movement key
and the modifiers mean nothing to it.
The press history
recentPresses() keeps the last PRESS_HISTORY keys the DEVICE
reported going down, each with the modifiers held at the time and when
it happened. It answers the question that is normally asked afterwards
-- did that key ever arrive? was the combo performed the way it was
described? -- instead of needing somebody to catch the moment. The macOS
fault above was found with it in one reading, after three rounds of
guessing at it.
It is also what a CONSECUTIVE combo would be matched on: one key after another inside a time window. That matcher does not exist yet; the record it needs does.
Applications that are pointed at rather than steered
The model above is what a game wants. An application whose cursor is the
instrument -- one where clicking is how things are chosen -- wants the
opposite, and setPointerLockOnClick(false) gives it: no click ever
takes the pointer, and the cursor stays on screen for good.
It has to be a deliberate choice, because it is a trade. Mouse-look reads the locked pointer's motion, so switching pointer lock off switches mouse-look off with it; a tool that does this drives its camera from keys, or from a drag it interprets itself.
Without it the first click is the last one the user can aim: the cursor disappears on the way in, and every click after that is made blind at a window that no longer shows where the pointer is.
OkCore::getInput()->setPointerLockOnClick(false);
The frame's mouse pan delta (panX / panY) follows the same rule from
the other side. It is fed while the cursor is captured — or, in an
application that has switched capture off altogether, always. Otherwise
a delta that waited for a capture which never comes would never arrive,
and every pan-style controller would sit still in exactly the kind of
application that most wants one. Applications that leave capture on are
unaffected: for them the condition is still "captured", so a mouse
crossing the window before any click moves nothing.
Mouse-look keeps needing the capture either way. A free cursor aimed at a menu would be swinging the view along behind it.
OkInputState fields
getState() returns a struct with ready-to-use flags. Movement: forward, backward, strafeLeft, strafeRight, plus the vertical nudge moveUp / moveDown (E / Q held). Rotation: turnLeft, turnRight, turnUp, turnDown. Mouse pan: panX / panY, the frame's raw mouse pixel delta (accumulated by the cursor callback while the cursor is captured, or always when the application has switched capture off; consumed by pan-style controllers). Edge-triggered actions (true only on the frame first pressed): action1, action2, action3, action4. Camera selection: changeCamera (-1 if none). And exit, set when the user asks to quit (ESC while the cursor is already released; a captured cursor consumes ESC to release first).
Example
void stepCallback(float deltaTime) {
OkInput *input = OkCore::getInput();
OkInputState state = input->getState();
if (state.exit) {
OkCore::askForExit();
return;
}
OkCamera *camera = OkCore::getCamera();
OkPoint forward = camera->getRotation().getForwardVector();
if (state.forward) {
camera->move(forward.x() * deltaTime,
forward.y() * deltaTime,
forward.z() * deltaTime);
}
}