reading input

goo has no input blob. you read the keyboard yourself, once per frame, and feed the result into your tree the same way you feed any other state: mutate a field, call Rebuild().

the helper for that is Goo.Input.KeyTracker. it wraps the engine's per-key query and hands you a list of every key that was just pressed this frame, plus a snapshot of the modifier keys.

the problem it solves

the engine exposes Sandbox.Input.Keyboard.Down( name ), which answers "is this one key down right now" for a single named key. there is no built-in "which keys were just pressed across the whole keyboard this frame". KeyTracker polls a catalog of keys once per frame, remembers what was down last frame, and surfaces the rising edges.

polling each frame

hold a KeyTracker in a field, call Poll() once per frame in OnUpdate, then read JustPressed and Modifiers. reset it in OnEnabled so a re-enable starts clean.

readonly KeyTracker _tracker = new() { ReemitHeldOnModifierRise = true };

protected override void OnEnabled()
{
    base.OnEnabled();
    _tracker.Reset();
}

protected override void OnUpdate()
{
    _tracker.Poll();

    foreach ( var key in _tracker.JustPressed )
        Log.Info( key.DisplayName );

    base.OnUpdate();
}

the OnUpdate plus Rebuild() clock is the same one the build method teaches: poll, update your fields, rebuild. the keystroke visualizer demo (Code/Demos/KeystrokeVisualizerUI.cs) sets its tracker up exactly like the field above, then routes JustPressed into its own grouping logic instead of logging.

what you get back

Poll() updates two members.

JustPressed is an IReadOnlyList<KeyDescriptor>: every key whose state rose from up to down on this frame. it is rebuilt each Poll(), so read it right after polling.

each KeyDescriptor describes one key:

field type what it is
EngineName string the name Sandbox.Input.Keyboard.Down accepts (lowercase for modifiers and f1..f12, uppercase for letters)
Class KeyClass Modifier, Printable, or Special
BaseGlyph char unshifted character, '\0' for non-printable keys
ShiftGlyph char shifted character, '\0' for non-printable keys
DisplayName string human label, e.g. "Ctrl", "Esc", "F1"

Modifiers is a ModifierState snapshot of the four modifier keys:

member type what it is
Ctrl bool control held
Shift bool shift held
Alt bool alt held
Meta bool the windows / meta key held
AnyNonShift bool Ctrl, Alt, or Meta held
Any bool any of the four held

when you only care about one key, skip the list walk: Pressed( engineName ) returns true if that named key had a rising edge this frame (just-pressed, not held). call Poll() first, same as the other reads.

the catalog: KnownKeys

a default KeyTracker() polls KnownKeys.All, a curated catalog of about eighty keys: the four modifiers, A to Z, the US-layout digits and symbols, common specials (space, enter, arrows, and so on), and F1 to F12. it is not exhaustive. non-US layouts and the numpad are out of scope.

to poll a narrower set, pass your own list to the constructor:

var arrows = new[]
{
    KnownKeys.All[0], // or build your own KeyDescriptor list
};
var tracker = new KeyTracker( arrows );

KnownKeys.Shift( baseGlyph, shiftDown ) maps a base glyph to its shifted form on a US layout ('a' to 'A', '1' to '!'), returning the input unchanged when shift is not down.

chords: ReemitHeldOnModifierRise

by default ReemitHeldOnModifierRise is false: each key reports its own rising edge and nothing more. that misses a common case. if you hold a letter and then press a modifier, the letter's edge already fired on an earlier frame, so the modifier-down frame reports only the modifier. you wanted a chord.

set ReemitHeldOnModifierRise = true and, on any frame where a modifier rises while a non-modifier key is already held, that held key re-emits as just-pressed. now "hold w, then press ctrl" produces a ctrl + w chord rather than a missed combo. the keystroke visualizer turns this on.

see also