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.)

onClick is not one of these. onClick — and onValidate / 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 for onMouseUp only 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:

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:

Next