Events & binding

onClick and friends

Activatable widgets take an onClick option — the simplest way to respond to a press:

app.TButton({ 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.

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

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, Enter, Leave, Wheel) x, y (within the widget), screenX, screenY, button, 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