Events & binding
onClick and friends
Activatable widgets take an onClick option — the simplest way to respond to a
press:
app.Button({ text: 'Go', onClick: () => console.log('clicked') });
The handler receives the widget itself as its argument. CheckButton,
RadioButton, scales, and similar widgets follow the same pattern.
Named events — on() and onXxx props
For the everyday pointer, wheel, and focus events you don't have to remember Tk
sequences. Every widget has an on(name, handler) method keyed by a friendly
name, and the same names work as onXxx options when you build the widget:
// imperative
btn.on('enter', () => (btn.style = 'Accent.TButton'));
btn.on('leave', () => (btn.style = 'TButton'));
// declarative — set them in the constructor, alongside normal options
app.Label({
text: 'Scroll me',
onWheel: (e) => console.log(e.delta > 0 ? 'up' : 'down'),
onRightClick: (e) => menu.popup(e.screenX, e.screenY),
});
Name (on('…') / onXxx prop) |
Tk sequence |
|---|---|
enter / onEnter |
<Enter> |
leave / onLeave |
<Leave> |
move / onMove |
<Motion> |
mousedown / onMouseDown |
<ButtonPress-1> |
mouseup / onMouseUp |
<ButtonRelease-1> |
doubleclick / onDoubleClick |
<Double-Button-1> |
rightclick / onRightClick |
<Button-3> |
wheel / onWheel |
<MouseWheel> |
focus / onFocus |
<FocusIn> |
blur / onBlur |
<FocusOut> |
These are pure sugar over bind (below): the handler gets the same event
object, and re-using a name replaces the previous handler (it doesn't stack).
on() is a strict superset of bind — an unrecognised name is passed through as
a raw sequence, so btn.on('<Control-Key-s>', save) works too.
enter / leave get mouseenter/mouseleave semantics: they fire only when
the pointer crosses the widget's own boundary, not when it moves onto a child
widget — so hover state on a container that has children doesn't flicker. (Raw
widget.bind('<Enter>', …) keeps Tk's unfiltered crossing behaviour, which fires
again on every child crossing.)
onClickis not one of these.onClick— andonValidate/onPost/onInvalid— are native widget command options, not pointer bindings. They fire with the widget as the argument (no event object) and also respond to keyboard activation (Space / Return), which is what you usually want for a button. Reach foronMouseUponly when you specifically need the raw pointer release instead.
The focused widget — app.focused
app.focused reports which widget currently holds keyboard focus — or
undefined when nothing does (or the focused widget isn't a ScriptWeaver
widget). It's handy for a global shortcut or menu command that should act on
"whatever's focused":
// Which widget has focus right now?
const w = app.focused;
console.log(w ? `focus on ${w._id}` : 'nothing focused');
To move focus to a widget, call widget.focus() (see
Widget basics).
bind — any event
For anything beyond a plain click — keys, mouse, hover, focus, resize — use
widget.bind(sequence, handler):
entry.bind('<Return>', () => submit());
list.bind('<Double-1>', () => open(list.curselection()));
btn.bind('<Enter>', () => (btn.style = 'Accent.TButton'));
A sequence is a Tk event spec in angle brackets — for example <Button-1>
(left click), <Double-1> (double click), <Button-3> (right click), <Key>,
<Return>, <Escape>, <Enter> / <Leave> (pointer in/out), <Configure>
(resize/move), <FocusIn> / <FocusOut>. Double angle brackets are virtual
events (<<TreeviewSelect>>, <<ThemeChanged>>, <<DateEntrySelected>>).
Virtual events are only delivered to widgets that are actually on screen — bind hooks like
<<ThemeChanged>>on a widget you have laid out (packed / gridded), not on a hidden helper.
The event object
Your handler is called with an event object whose fields depend on the event kind — ScriptWeaver fills in only the relevant ones:
| Event kind | Fields |
|---|---|
Mouse (Button, Motion) |
x, y (within the widget), screenX, screenY, button, state |
Pointer crossing (Enter, Leave) |
x, y, screenX, screenY, state, detail (Tk crossing detail, e.g. NotifyInferior) |
Mouse wheel (MouseWheel) |
x, y, screenX, screenY, delta (signed scroll amount), state |
Keyboard (Key…) |
keyCode, key, char, state |
Configure (resize / move) |
width, height, x, y |
| Focus | target, detail |
Every event object also carries widget (the target's path) and type (the
sequence).
canvas.bind('<Button-1>', (e) => {
console.log(`clicked at ${e.x}, ${e.y}`);
});
app.bind('<Configure>', (e) => {
console.log(`resized to ${e.width}×${e.height}`);
});
Mouse coordinates: widget-relative vs screen
A mouse event carries two coordinate pairs, and the difference matters the moment a widget moves:
x,yare measured relative to the event's widget, as it sat when Tk generated the event. If that widget then moves, the origin they're measured from moves with it.screenX,screenYare absolute screen coordinates — independent of any widget's position, so they stay correct across moves.
This is the classic trap in drag handlers. The tempting-but-wrong approach is
to move the widget and then reconstruct the pointer as
widgetAbsolutePosition + e.x — but e.x was measured against the widget's
old spot, so the drag distance gets counted twice and the cursor races ahead of
what it's dragging. (This is ordinary Tk geometry, not a ScriptWeaver quirk — the
same bug exists in plain Tcl/Tk.)
The robust pattern: snapshot a reference point once on press, then drive the move
from the screen-space delta since then — never from x / y:
let drag = null;
box.bind('<ButtonPress-1>', (e) => {
// Where the pointer started, and where the box started.
drag = { sx: e.screenX, sy: e.screenY, x0: boxX, y0: boxY };
});
box.bind('<B1-Motion>', (e) => {
if (!drag) return;
// New position = start position + how far the pointer moved on screen.
// Immune to the box having already moved, and to handler-dispatch latency.
boxX = drag.x0 + (e.screenX - drag.sx);
boxY = drag.y0 + (e.screenY - drag.sy);
box.place.configure({ x: boxX, y: boxY });
});
box.bind('<ButtonRelease-1>', () => (drag = null));
Rule of thumb: if your math has to stay correct while the thing under the pointer
is moving, use screenX / screenY, not x / y.
Handlers run asynchronously
Event handlers are dispatched to your JavaScript without blocking the UI — the window keeps repainting and stays responsive even while a handler runs. Two practical consequences:
- There is no
preventDefault/stopPropagation. A handler observes an event; it can't cancel the widget's built-in behaviour or stop the event from reaching other bindings. - For input validation (rejecting keystrokes as they're typed), use an entry's
onValidateoption rather than a<Key>binding.
Related
- Reactive values — to react to a value changing rather than a UI event,
bind a variable and use its
onChange. - Window close — handle the window-manager close button with
win.wm.protocol('WM_DELETE_WINDOW', () => …).