Input reaches a mod in two ways. Inside a widget, the node that took the tap, click, wheel notch, scrub or edit carries the function that handles it. Outside a widget, a mod can claim a global keyboard shortcut under perch.input.hotkeys. A mod never sees raw keystrokes.
Input on nodes
Any node accepts these props. A node with any of them becomes interactive (the island stops being click-through under it); a node with only on_scroll does not capture clicks.
| Prop | Fires on | ctx carries |
|---|---|---|
on_click | a left tap | button = "left" |
on_right | a right click; the island’s own menu is suppressed under it | button = "right" |
on_middle | a middle click | button = "middle" |
on_scroll | a wheel notch, at most once per 80 ms per node | steps, signed, positive is up |
on_seek | a scrub on a ui.bar with seek = true: live while dragging, once more on release | frac, 0..1 |
on_change | an edit in a ui.field, debounced 800 ms and flushed on blur | (text, ctx) |
ui.button { glyph = ui.glyph.play, on_click = function(ctx: input_context_t) toggle() end }
ui.column { on_scroll = function(ctx: input_context_t) perch.audio.volume.set(level + (ctx.steps or 0) * 2) end, ... }
ui.bar { value = f.pos_frac, seek = true, knob = true, on_seek = function(ctx: input_context_t) seek_to(ctx.frac or 0) end }
ui.field { value = f.text, on_change = function(text: string, ctx: input_context_t) perch.storage.set("text", text) end }
Every handler receives an input_context_t. It always carries widget (the handle) and, for the click props, button. Inside a repeater item it also carries index (1-based) and item (the element’s data, present while the list rows are resident):
ui.list { items = f.apps, item = function(it: app_refs_t): node_t
return ui.image { src = it.icon, hover = ui.hover.lift,
on_click = function(ctx: input_context_t) perch.apps.launch(entries[ctx.index or 1]) end }
end }
A display function runs when the widget is placed, often before the verbs it calls exist. Look the verb up at click time (on_click = function() act.play_pause() end) rather than capturing it (on_click = act.play_pause, which captures nil).
Hover
| Prop | Meaning |
|---|---|
hover = ui.hover.lift | the node pops up under the pointer (dock icons) |
hover = ui.hover.soft | the node brightens slightly (button weight) |
hover = ui.hover.scope | the node defines a hover region for the two props below |
hover_show, hover_hide | on descendants of a scope: fade in, or out, while the scope is hovered |
Hotkeys
Scope input.hotkeys. A mod claims a combination, Windows routes it to Perch from anywhere on the machine, and the mod hears on_press on the handle it got back. Claimed shortcuts are listed in Settings so the user can see what a mod took.
perch.input.hotkeys.register
perch.input.hotkeys.register(combo: hotkey_t): hotkey_handle_t?
local capture = perch.input.hotkeys.register { ctrl = true, shift = true, key = key.P }
combo is any of ctrl, shift, alt, win set to true, plus key, a member of the global key enum (key.P, key.F4, key.SPACE). A missing or non-enum key raises.
Returns the handle, or nil when another application already owns that combination. Windows allows one owner per shortcut system-wide, and the first claimant wins. It is not an error and not recoverable by retrying: tell the user and offer another combination.
| Member | Meaning |
|---|---|
handle.on_press | signal_t<>; fires once per press, not repeatedly while held |
handle:release() | gives the claim up. Releasing twice is harmless |
Release the old handle before registering a new combination, otherwise your own mod is the application that already owns it. Perch releases anything a mod forgot when it stops.
--!strict
type settings_t = { letter: string? }
local capture: hotkey_handle_t? = nil
perch.on_configure:connect(function(s: settings_t)
if capture then capture:release() end
capture = perch.input.hotkeys.register { ctrl = true, shift = true, key = key[s.letter or "N"] }
if capture == nil then
w:present({ text = "Shortcut unavailable. Pick another in Settings." })
else
capture.on_press:connect(function() w:present({ text = "Captured" }) end)
end
end)
Types
export type input_context_t = {
widget: any_widget_t,
index: number?,
item: any?,
frac: number?,
steps: number?,
button: mouse_button_t?,
}
export type mouse_button_t = "left" | "right" | "middle"
What every input handler receives. index and item inside a repeater item; frac on a seek; steps on a wheel notch; button on the click props.
export type input_handler_t = (ctx: input_context_t) -> ()
export type change_handler_t = (text: string, ctx: input_context_t) -> ()
The type of on_click, on_right, on_middle, on_scroll and on_seek; and of a ui.field’s on_change.
export type input_props_t = {
on_click: input_handler_t?,
on_right: input_handler_t?,
on_middle: input_handler_t?,
on_scroll: input_handler_t?,
hover: enum_t<"hover">?,
hover_show: boolean?,
hover_hide: boolean?,
}
The input props every node accepts; node_props_t includes them.
export type hotkey_t = { ctrl: boolean?, shift: boolean?, alt: boolean?, win: boolean?, key: enum_t<"key"> }
export type hotkey_handle_t = { on_press: signal_t<>, release: (self: hotkey_handle_t) -> () }
declare key: { [string]: enum_t<"key"> }
A shortcut, the handle register returns, and the keyboard enum.